back to index

Webpack Config Notes

A few webpack pitfalls hit while wiring up ghoti-cli's fully-static React template.

published Mar 28, 2018 tags #javascript #webpack

~/posts/webpack-config-notes $ cat post.md

/ LANG EN / 中文
/ THEME / /

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"),
}

ExtractTextWebpackPlugin is replaced by MiniCssExtractPlugin in 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.

back to index