Caching
So we're using webpack to bundle our modular application which yields a deployable /dist
directory. Once the contents of /dist
have been deployed to a server, clients (typically browsers) will hit that server to grab the site and its assets. The last step can be time consuming, which is why browsers use a technique called caching. This allows sites to load faster with less unnecessary network traffic. However, it can also cause headaches when you need new code to be picked up.
This guide focuses on the configuration needed to ensure files produced by webpack compilation can remain cached unless their content has changed.
Output Filenames
We can use the output.filename
substitutions setting to define the names of our output files. Webpack provides a method of templating the filenames using bracketed strings called substitutions. The [contenthash]
substitution will add a unique hash based on the content of an asset. When the asset's content changes, [contenthash]
will change as well.
Let's get our project set up using the example from getting started with the plugins
from output management, so we don't have to deal with maintaining our index.html
file manually:
project
webpack-demo |- package.json |- webpack.config.js |- /dist |- /src |- index.js |- /node_modules
webpack.config.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/index.js', plugins: [ new HtmlWebpackPlugin({ - title: 'Output Management', + title: 'Caching', }), ], output: { - filename: 'bundle.js', + filename: '[name].[contenthash].js', path: path.resolve(__dirname, 'dist'), clean: true, }, };
Running our build script, npm run build
, with this configuration should produce the following output:
... Asset Size Chunks Chunk Names main.7e2c49a622975ebd9b7e.js 544 kB 0 [emitted] [big] main index.html 197 bytes [emitted] ...
As you can see the bundle's name now reflects its content (via the hash). If we run another build without making any changes, we'd expect that filename to stay the same. However, if we were to run it again, we may find that this is not the case:
... Asset Size Chunks Chunk Names main.205199ab45963f6a62ec.js 544 kB 0 [emitted] [big] main index.html 197 bytes [emitted] ...
This is because webpack includes certain boilerplate, specifically the runtime and manifest, in the entry chunk.
Extracting Boilerplate
As we learned in code splitting, the SplitChunksPlugin
can be used to split modules out into separate bundles. Webpack provides an optimization feature to split runtime code into a separate chunk using the optimization.runtimeChunk
option. Set it to single
to create a single runtime bundle for all chunks:
webpack.config.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/index.js', plugins: [ new HtmlWebpackPlugin({ title: 'Caching', }), ], output: { filename: '[name].[contenthash].js', path: path.resolve(__dirname, 'dist'), clean: true, }, + optimization: { + runtimeChunk: 'single', + }, };
Let's run another build to see the extracted runtime
bundle:
Hash: 82c9c385607b2150fab2 Version: webpack 4.12.0 Time: 3027ms Asset Size Chunks Chunk Names runtime.cc17ae2a94ec771e9221.js 1.42 KiB 0 [emitted] runtime main.e81de2cf758ada72f306.js 69.5 KiB 1 [emitted] main index.html 275 bytes [emitted] [1] (webpack)/buildin/module.js 497 bytes {1} [built] [2] (webpack)/buildin/global.js 489 bytes {1} [built] [3] ./src/index.js 309 bytes {1} [built] + 1 hidden module
It's also good practice to extract third-party libraries, such as lodash
or react
, to a separate vendor
chunk as they are less likely to change than our local source code. This step will allow clients to request even less from the server to stay up to date. This can be done by using the cacheGroups
option of the SplitChunksPlugin
demonstrated in Example 2 of SplitChunksPlugin. Lets add optimization.splitChunks
with cacheGroups
with next params and build:
webpack.config.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/index.js', plugins: [ new HtmlWebpackPlugin({ title: 'Caching', }), ], output: { filename: '[name].[contenthash].js', path: path.resolve(__dirname, 'dist'), clean: true, }, optimization: { runtimeChunk: 'single', + splitChunks: { + cacheGroups: { + vendor: { + test: /[\\/]node_modules[\\/]/, + name: 'vendors', + chunks: 'all', + }, + }, + }, }, };
Let's run another build to see our new vendor
bundle:
... Asset Size Chunks Chunk Names runtime.cc17ae2a94ec771e9221.js 1.42 KiB 0 [emitted] runtime vendors.a42c3ca0d742766d7a28.js 69.4 KiB 1 [emitted] vendors main.abf44fedb7d11d4312d7.js 240 bytes 2 [emitted] main index.html 353 bytes [emitted] ...
We can now see that our main
bundle does not contain vendor
code from node_modules
directory and is down in size to 240 bytes
!
Module Identifiers
Let's add another module, print.js
, to our project:
project
webpack-demo |- package.json |- webpack.config.js |- /dist |- /src |- index.js + |- print.js |- /node_modules
print.js
+ export default function print(text) { + console.log(text); + };
src/index.js
import _ from 'lodash'; + import Print from './print'; function component() { const element = document.createElement('div'); // Lodash, now imported by this script element.innerHTML = _.join(['Hello', 'webpack'], ' '); + element.onclick = Print.bind(null, 'Hello webpack!'); return element; } document.body.appendChild(component());
Running another build, we would expect only our main
bundle's hash to change, however...
... Asset Size Chunks Chunk Names runtime.1400d5af64fc1b7b3a45.js 5.85 kB 0 [emitted] runtime vendor.a7561fb0e9a071baadb9.js 541 kB 1 [emitted] [big] vendor main.b746e3eb72875af2caa9.js 1.22 kB 2 [emitted] main index.html 352 bytes [emitted] ...
... we can see that all three have. This is because each module.id
is incremented based on resolving order by default. Meaning when the order of resolving is changed, the IDs will be changed as well. To recap:
- The
main
bundle changed because of its new content. - The
vendor
bundle changed because itsmodule.id
was changed. - And, the
runtime
bundle changed because it now contains a reference to a new module.
The first and last are expected, it's the vendor
hash we want to fix. Let's use optimization.moduleIds
with 'deterministic'
option:
webpack.config.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/index.js', plugins: [ new HtmlWebpackPlugin({ title: 'Caching', }), ], output: { filename: '[name].[contenthash].js', path: path.resolve(__dirname, 'dist'), clean: true, }, optimization: { + moduleIds: 'deterministic', runtimeChunk: 'single', splitChunks: { cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all', }, }, }, }, };
Now, despite any new local dependencies, our vendor
hash should stay consistent between builds:
... Asset Size Chunks Chunk Names main.216e852f60c8829c2289.js 340 bytes 0 [emitted] main vendors.55e79e5927a639d21a1b.js 69.5 KiB 1 [emitted] vendors runtime.725a1a51ede5ae0cfde0.js 1.42 KiB 2 [emitted] runtime index.html 353 bytes [emitted] Entrypoint main = runtime.725a1a51ede5ae0cfde0.js vendors.55e79e5927a639d21a1b.js main.216e852f60c8829c2289.js ...
And let's modify our src/index.js
to temporarily remove that extra dependency:
src/index.js
import _ from 'lodash'; - import Print from './print'; + // import Print from './print'; function component() { const element = document.createElement('div'); // Lodash, now imported by this script element.innerHTML = _.join(['Hello', 'webpack'], ' '); - element.onclick = Print.bind(null, 'Hello webpack!'); + // element.onclick = Print.bind(null, 'Hello webpack!'); return element; } document.body.appendChild(component());
And finally run our build again:
... Asset Size Chunks Chunk Names main.ad717f2466ce655fff5c.js 274 bytes 0 [emitted] main vendors.55e79e5927a639d21a1b.js 69.5 KiB 1 [emitted] vendors runtime.725a1a51ede5ae0cfde0.js 1.42 KiB 2 [emitted] runtime index.html 353 bytes [emitted] Entrypoint main = runtime.725a1a51ede5ae0cfde0.js vendors.55e79e5927a639d21a1b.js main.ad717f2466ce655fff5c.js ...
We can see that both builds yielded 55e79e5927a639d21a1b
in the vendor
bundle's filename.
Conclusion
Caching can be complicated, but the benefit to application or site users makes it worth the effort. See the Further Reading section below to learn more.
Further Reading
© JS Foundation and other contributors
Licensed under the Creative Commons Attribution License 4.0.
https://webpack.js.org/guides/caching