Skip to main content

Cache busting

Back in the days when cache-control headers were cool, controlling cached assets life cycle was about understanding things 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 particular reason we are not going to be using HTTP protocol based mechanism of controlling assets life cycle.

Invalidation

Sooner than 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 all about. But it's not about invalidating the content at all. Instead, we just let the browser know that a portion of the web content is newer on the server that the one we deployed last time. The rest of invalidation is handled by the browser itself and it works the same for all the browsers out there.

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 file names could be controlled via the configuration itself. Besides of giving custom names that convey more meaning, specific substitutions could 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 could be controlled as well. For example, first 8 digits content hash value would be declared as [contenthash:8]

So, just by adding these substitutions we can control file names using specific webpack build output. We need to do this only for production builds thought, in local dev scenarios it just slows dows 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 file names 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 webpack build hash as a version URL parameter to the manifest file makes sure manifest gets refreshed every new webpack build.

Final version

Reference implementation