Webpack Config Notes
A few webpack pitfalls hit while wiring up ghoti-cli's fully-static React template.
~/posts/webpack-config-notes $ cat post.md
Overview
Webpack configs are full of small landmines. Notes below from building ghoti-cli’s “fully-static React” template.
What’s ghoti-cli
A project management CLI I use for myself — it bundles a dozen-ish quickstart templates and a progress tracker. The template that’s caused me the most pain is its “fully-static React” config.
What’s “fully-static React”
The idea: write the app as a normal React dev project — HMR, router, the usual. At build time, use server-side rendering to render each page to a string and stash it in memory. An Express server then serves those cached strings on request.
Because the server itself is webpack-bundled, source changes can’t hot reload — that’s fine. The server is set up to listen across multiple workers, so during dev iterations you can restart instances in rotation without taking the site down.
Gotchas
HMR entry config
Here’s the entry block in webpack.dev.js:
entry: [
"react-hot-loader/patch",
"webpack-dev-server/client",
"webpack/hot/only-dev-server",
APP_DIR + "/index.dev.tsx",
],
The first, second, and fourth entries are self-explanatory. The third reads like “accept hot updates from the dev server” — at first glance redundant. Why not drop it?
Reading the source, that third line is what actually makes HMR work. The first entry handles full-page refresh when the change is too big to swap in-place; the third is the HMR runtime entry.
The takeaway: leave the HMR entry block alone.
Image bundling when targeting Node
Bundling images is one of webpack’s bread-and-butter features — inlining a small logo skips an HTTP request. A typical rule:
{
test: /\.(jpg|png|gif|webp)$/,
loader: "url-loader?limit=8192",
}
This lets you require an image directly. limit is in bytes: images
≤ 8 KB get inlined as base64; anything larger gets copied out, renamed
to a hash, and require resolves to the hashed path.
Why this is worth a note: you reach for url-loader over a plain
external image when you want to reuse the image and cut request count
— think a logo or a button. So pick the limit carefully: too low and
too many images escape to external requests; too high and the bundle
bloats.
Splitting CSS out of JS
For server-rendered HTML strings, we want to avoid baking JS into the
output (ghoti-cli ships a tiny external-dependency-free JS runtime
that covers most non-interactive React rendering). CSS is also better
off not being inlined into a <style> tag.
This is the config I used to split CSS out:
{
test: /\.sass$/,
use: ExtractTextWebpackPlugin.extract("css-loader?sourceMap!sass-loader?sourceMap"),
}
ExtractTextWebpackPluginis replaced byMiniCssExtractPluginin webpack 4+. The form above is webpack 3-era.
The “magic string”
css-loader?sourceMap!sass-loader?sourceMap looks suspicious. The
naive thing to try is:
"css-loader!post-css!sass-loader";
That breaks. First, post-css can’t be run inside this plugin.
Second, sourceMap isn’t optional here — ExtractTextWebpackPlugin
uses the dependency graph it implies to emit CSS correctly. Just copy
the working form.
Remember to also instantiate the plugin in plugins:
plugins: [
new ExtractTextWebpackPlugin("bundle.css"),
// or with webpack's name interpolation
new ExtractTextWebpackPlugin("[name].[hash].css"),
];
Don’t use style-loader together with this
For projects not doing the static-Node setup, webpack is usually configured as:
{
test: /\.sass$/,
use: [
"style-loader",
"css-loader",
"sass-loader",
],
},
style-loader injects CSS into React’s className, achieving the
“JS-only” effect. When ExtractTextPlugin is in play, style-loader
has to go.