Cache busting
Back in the day, when Cache-Control headers were king, controlling a cached asset's life cycle was about understanding directives like no-cache, no-store, public, private, max-age, etc. Nice idea, but unfortunately browsers have different opinions about how to implement it. For that reason, we are not going to use an HTTP‑based mechanism to control the asset life cycle.
Invalidation #
Sooner or later, the content becomes stale (content in a general sense: HTML, JS, CSS, images, etc.). After all, every deployment to production makes some portion of the web content stale. That portion has to be invalidated because it was cached by the browser as an optimization technique. That is what cache busting is about. But it's not about invalidating the content directly. Instead, we let the browser know that a portion of the web content is newer on the server than the one it cached last time. The rest of the invalidation is handled by the browser itself, and it works the same across browsers.
So how do we let the browser know that some content changed? Simple, we just change file names for the files representing the portion of the content that changed. Enter, webpack long term caching.
Webpack configuration #
Webpack output filenames can be controlled via configuration. Besides giving custom names that convey more meaning, specific substitutions can be used to inline internal build statistics:
[hash]- The hash of the module identifier[contenthash]- The hash of the content of a file[chunkhash]- The hash of the chunk content
The format of the hash value can be controlled as well. For example, the first 8 digits of a content hash would be declared as
[contenthash:8].
So, by adding these substitutions we can control filenames using specific webpack build output. We need to do this only for production builds though; in local dev scenarios it just slows down the process.
webpack/parts.ts #
Change webpack/parts.ts:
rules: {
images: (name?: string) => ({
test: /\.(png|jpg|gif|bmp)$/,
exclude: /favicons/,
use: [
{
loader: 'url-loader',
options: {
limit: 10000,
name: './img/[name].[ext]',
...(name ? { name } : {})
}
}]
}) as RuleSetRule,
}
We are changing rules for images and favicons to take optional file name as a parameter.
webpack/webpack.config.dev.ts #
Change webpack/webpack.config.dev.ts:
rules: [
parts.rules.babel,
parts.rules.images(),
// ...
]
webpack/webpack.config.ts #
Change webpack/webpack.config.ts:
output: {
...parts.output,
filename: '[name].bundle.[contenthash:8].js',
}
// ...
rules: [
parts.rules.babel,
parts.rules.images('./img/[name].[contenthash:8].[ext]')
// ...
{
test: /\.(woff|woff2)$/,
use: [{
loader: 'file-loader',
options: {
name: './fonts/[name].[contenthash:8].[ext]'
}
}]
}
],
// ...
plugins: [
...parts.plugins,
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].css',
})
// ...
]
We are overriding output filenames for all the assets generated:
'[name].bundle.[contenthash:8].js'- all the JavaScript bundles: main, vendors, chunks'./img/[name].[contenthash:8].[ext]'- image assets'css/[name].[contenthash:8].css'- all CSS bundles: main, vendor chunks
webpack/index.html #
Change webpack/index.html:
...
<link rel="manifest" href="/manifest.json?v=<%= compilation.getStats().hash %>">
...
Adding the webpack build hash as a version URL parameter to the manifest file ensures the manifest gets refreshed on every new webpack build.