The examples in this guide stem fromgetting started,output management andcode splitting.
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 calledcaching. 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.
We can use theoutput.filenamesubstitutions setting to define the names of our output files. Webpack provides a method of templating the filenames using bracketed strings calledsubstitutions. 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 fromgetting started with theplugins fromoutput management, so we don't have to deal with maintaining ourindex.html file manually:
project
webpack-demo|- package.json|- package-lock.json|- webpack.config.js|- /dist|- /src |- index.js|- /node_moduleswebpack.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 Namesmain.7e2c49a622975ebd9b7e.js544 kB0[emitted][big] main index.html197 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 Namesmain.205199ab45963f6a62ec.js544 kB0[emitted][big] main index.html197 bytes[emitted]...This is because webpack includes certain boilerplate, specifically the runtime and manifest, in the entry chunk.
Output may differ depending on your current webpack version. Newer versions may not have all the same issues with hashing as some older versions, but we still recommend the following steps to be safe.
As we learned incode splitting, theSplitChunksPlugin can be used to split modules out into separate bundles. Webpack provides an optimization feature to split runtime code into a separate chunk using theoptimization.runtimeChunk option. Set it tosingle 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 extractedruntime bundle:
Hash: 82c9c385607b2150fab2Version: webpack4.12.0Time: 3027ms Asset Size Chunks Chunk Namesruntime.cc17ae2a94ec771e9221.js1.42 KiB0[emitted] runtime main.e81de2cf758ada72f306.js69.5 KiB1[emitted] main index.html275 bytes[emitted][1](webpack)/buildin/module.js497 bytes{1}[built][2](webpack)/buildin/global.js489 bytes{1}[built][3] ./src/index.js309 bytes{1}[built] +1 hidden moduleIt's also good practice to extract third-party libraries, such aslodash orreact, to a separatevendor 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 thecacheGroups option of theSplitChunksPlugin demonstrated inExample 2 of SplitChunksPlugin. Lets addoptimization.splitChunks withcacheGroups 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 newvendor bundle:
... Asset Size Chunks Chunk Namesruntime.cc17ae2a94ec771e9221.js1.42 KiB0[emitted] runtimevendors.a42c3ca0d742766d7a28.js69.4 KiB1[emitted] vendors main.abf44fedb7d11d4312d7.js240 bytes2[emitted] main index.html353 bytes[emitted]...We can now see that ourmain bundle does not containvendor code fromnode_modules directory and is down in size to240 bytes!
Let's add another module,print.js, to our project:
project
webpack-demo|- package.json|- package-lock.json|- webpack.config.js|- /dist|- /src |- index.js+ |- print.js|- /node_modulesprint.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 ourmain bundle's hash to change, however...
... Asset Size Chunks Chunk Names runtime.1400d5af64fc1b7b3a45.js5.85 kB0[emitted] runtime vendor.a7561fb0e9a071baadb9.js541 kB1[emitted][big] vendor main.b746e3eb72875af2caa9.js1.22 kB2[emitted] main index.html352 bytes[emitted]...... we can see that all three have. This is because eachmodule.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:
main bundle changed because of its new content.vendor bundle changed because itsmodule.id was changed.runtime bundle changed because it now contains a reference to a new module.The first and last are expected, it's thevendor hash we want to fix. Let's useoptimization.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, ourvendor hash should stay consistent between builds:
... Asset Size Chunks Chunk Names main.216e852f60c8829c2289.js340 bytes0[emitted] mainvendors.55e79e5927a639d21a1b.js69.5 KiB1[emitted] vendorsruntime.725a1a51ede5ae0cfde0.js1.42 KiB2[emitted] runtime index.html353 bytes[emitted]Entrypoint main= runtime.725a1a51ede5ae0cfde0.js vendors.55e79e5927a639d21a1b.js main.216e852f60c8829c2289.js...And let's modify oursrc/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.js274 bytes0[emitted] mainvendors.55e79e5927a639d21a1b.js69.5 KiB1[emitted] vendorsruntime.725a1a51ede5ae0cfde0.js1.42 KiB2[emitted] runtime index.html353 bytes[emitted]Entrypoint main= runtime.725a1a51ede5ae0cfde0.js vendors.55e79e5927a639d21a1b.js main.ad717f2466ce655fff5c.js...We can see that both builds yielded55e79e5927a639d21a1b in thevendor bundle's filename.
Caching can be complicated, but the benefit to application or site users makes it worth the effort. See theFurther Reading section below to learn more.