The examples in this guide stem from getting started, output management and code 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 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 contents has changed.
A simple way to ensure the browser picks up changed files is by using output.filename
substitutions. The [hash]
substitution can be used to include a build-specific hash in the filename, however it's even better to use the [contenthash]
substitution which is the hash of the content of a file, which is different for each asset.
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 CleanWebpackPlugin = require('clean-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/index.js', plugins: [ new CleanWebpackPlugin(['dist']), new HtmlWebpackPlugin({ - title: 'Output Management' + title: 'Caching' }) ], output: { - filename: 'bundle.js', + filename: '[name].[contenthash].js', path: path.resolve(__dirname, 'dist') } };
Running our build script, npm run build
, with this configuration should produce the following output:
Hash: f7a289a94c5e4cd1e566 Version: webpack 3.5.1 Time: 835ms Asset Size Chunks Chunk Names main.7e2c49a622975ebd9b7e.js 544 kB 0 [emitted] [big] main index.html 197 bytes [emitted] [0] ./src/index.js 216 bytes {0} [built] [2] (webpack)/buildin/global.js 509 bytes {0} [built] [3] (webpack)/buildin/module.js 517 bytes {0} [built] + 1 hidden module Child html-webpack-plugin for "index.html": 1 asset [2] (webpack)/buildin/global.js 509 bytes {0} [built] [3] (webpack)/buildin/module.js 517 bytes {0} [built] + 2 hidden modules
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:
Hash: f7a289a94c5e4cd1e566 Version: webpack 3.5.1 Time: 835ms Asset Size Chunks Chunk Names main.205199ab45963f6a62ec.js 544 kB 0 [emitted] [big] main index.html 197 bytes [emitted] [0] ./src/index.js 216 bytes {0} [built] [2] (webpack)/buildin/global.js 509 bytes {0} [built] [3] (webpack)/buildin/module.js 517 bytes {0} [built] + 1 hidden module Child html-webpack-plugin for "index.html": 1 asset [2] (webpack)/buildin/global.js 509 bytes {0} [built] [3] (webpack)/buildin/module.js 517 bytes {0} [built] + 2 hidden modules
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 in code splitting, the SplitChunksPlugin
can be used to split modules out into separate bundles. webpack provides an optimization feature which does split out runtime code into a separate chunk(s) according to the options provided, simply use optimization.runtimeChunk
set to single
for creating one runtime bundle:
webpack.config.js
const path = require('path'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/index.js', plugins: [ new CleanWebpackPlugin(['dist']), new HtmlWebpackPlugin({ title: 'Caching' ], output: { filename: '[name].[contenthash].js', path: path.resolve(__dirname, 'dist') }, + 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
var path = require('path'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/index.js', plugins: [ new CleanWebpackPlugin(['dist']), new HtmlWebpackPlugin({ title: 'Caching' }), ], output: { filename: '[name].[contenthash].js', path: path.resolve(__dirname, 'dist') }, optimization: { - runtimeChunk: 'single' + runtimeChunk: 'single', + splitChunks: { + cacheGroups: { + vendor: { + test: /[\\/]node_modules[\\/]/, + name: 'vendors', + chunks: 'all' + } + } + } } };
Let's run another build to see our new vendor
bundle:
Hash: 213f57fc3bb5cb47c719 Version: webpack 4.12.0 Time: 475ms 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] [1] (webpack)/buildin/module.js 497 bytes {1} [built] [2] (webpack)/buildin/global.js 489 bytes {1} [built] [3] ./src/index.js 309 bytes {2} [built] + 1 hidden module
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
!
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() { var 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...
Hash: d38a06644fdbb898d795 Version: webpack 3.3.0 Time: 1445ms Asset Size Chunks Chunk Names vendor.a7561fb0e9a071baadb9.js 541 kB 0 [emitted] [big] vendor main.b746e3eb72875af2caa9.js 1.22 kB 1 [emitted] main manifest.1400d5af64fc1b7b3a45.js 5.85 kB 2 [emitted] manifest index.html 352 bytes [emitted] [1] ./src/index.js 421 bytes {1} [built] [2] (webpack)/buildin/global.js 509 bytes {0} [built] [3] (webpack)/buildin/module.js 517 bytes {0} [built] [4] ./src/print.js 62 bytes {1} [built] [5] multi lodash 28 bytes {0} [built] + 1 hidden module
... 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. So, to recap:
main
bundle changed because of its new content.vendor
bundle changed because its module.id
was changed.manifest
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. Luckily, there are two plugins we can use to resolve this issue. The first is the NamedModulesPlugin
, which will use the path to the module rather than a numerical identifier. While this plugin is useful during development for more readable output, it does take a bit longer to run. The second option is the HashedModuleIdsPlugin
, which is recommended for production builds:
webpack.config.js
const path = require('path'); const webpack = require('webpack'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/index.js', plugins: [ new CleanWebpackPlugin(['dist']), new HtmlWebpackPlugin({ title: 'Caching' }), + new webpack.HashedModuleIdsPlugin() ], output: { filename: '[name].[contenthash].js', path: path.resolve(__dirname, 'dist') }, optimization: { 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:
Hash: 17c37ce65c84b8ed5eb8 Version: webpack 4.16.2 Time: 637ms 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 [YuTi] (webpack)/buildin/module.js 497 bytes {1} [built] [tjUo] ./src/index.js + 1 modules 408 bytes {0} [built] | ./src/index.js 341 bytes [built] | ./src/print.js 62 bytes [built] [yLpj] (webpack)/buildin/global.js 489 bytes {1} [built] + 1 hidden module Child html-webpack-plugin for "index.html": 1 asset Entrypoint undefined = index.html [YuTi] (webpack)/buildin/module.js 497 bytes {0} [built] [yLpj] (webpack)/buildin/global.js 489 bytes {0} [built] + 2 hidden modules
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() { var 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:
Hash: 70fb9e00dee0bada797d Version: webpack 4.16.2 Time: 875ms 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 [YuTi] (webpack)/buildin/module.js 497 bytes {1} [built] [tjUo] ./src/index.js 347 bytes {0} [built] [yLpj] (webpack)/buildin/global.js 489 bytes {1} [built] + 1 hidden module Child html-webpack-plugin for "index.html": 1 asset Entrypoint undefined = index.html [YuTi] (webpack)/buildin/module.js 497 bytes {0} [built] [yLpj] (webpack)/buildin/global.js 489 bytes {0} [built] + 2 hidden modules
We can see that both builds yielded 55e79e5927a639d21a1b
in the vendor
bundle's filename.
Caching gets messy. Plain and simple. However the walk-through above should give you a running start to deploying consistent, cachable assets. See the Further Reading section below to learn more.
© JS Foundation and other contributors
Licensed under the Creative Commons Attribution License 4.0.
https://webpack.js.org/guides/caching