Caching
To enable long-term caching of static assets processed by webpack, you need to:
- Use
[chunkhash]
to create a content-based cache identifier for each file. - Retrieve filenames using the compilation stats when including files in HTML.
- Generate a chunk-manifest JSON file and embed it into the HTML page before loading resources.
- Ensure that the hash of the entry chunk containing the bootstrap code is not modified when its dependencies remain unchanged.
The Problem
Every time something in our code needs to be updated, it must be deployed to the server and re-downloaded by the client. This is very inefficient, especially when network conditions are poor. This is why browsers cache static resources.
This leads to a pitfall: if we release a new version without updating the filenames, the browser will assume the files haven’t changed, preventing clients from getting the latest resources.
A simple way to solve this is to tell the browser a new filename. Without webpack, we might use a build version to identify the update:
application.js?build=1
application.css?build=1
In webpack, this is also straightforward: every webpack build generates a unique hash that can be used to form filenames. The following configuration will generate two filenames with hashes:
// webpack.config.js
const path = require("path");
module.exports = {
entry: {
vendor: "./src/vendor.js",
main: "./src/index.js",
},
output: {
path: path.join(__dirname, "build"),
filename: "[name].[hash].js",
},
};
Running the webpack command will produce the following output:
Hash: 55e783391098c2496a8f
Version: webpack 1.10.1
Time: 58ms
Asset Size Chunks Chunk Names
main.55e783391098c2496a8f.js 1.43 kB 0 [emitted] main
vendor.55e783391098c2496a8f.js 1.43 kB 1 [emitted] vendor
[0] ./src/index.js 46 bytes {0} [built]
[0] ./src/vendor.js 40 bytes {1} [built]
However, the problem is that every time we recompile, all filenames change, causing the client to re-download the entire application code each time. So, how can we ensure that the client only downloads files that have changed?
Generating Unique Hashes for Each File
How can we ensure that a file’s filename doesn’t change with every compilation if its content hasn’t changed? For example, a code bundle that contains all our dependency libraries.
Webpack allows generating hashes based on file content. Here’s the updated configuration:
// webpack.config.js
module.exports = {
/*...*/
output: {
/*...*/
filename: "[name].[chunkhash].js",
},
};
This configuration will also generate two files, but each file will have its own hash:
main.155567618f4367cd1cb8.js 1.43 kB 0 [emitted] main
vendor.c2330c22cd2decb5da5a.js 1.43 kB 1 [emitted] vendor
Do not use
[chunkhash]
in development environments, as it will increase compilation time. Separate your development and production configurations, using[name].js
in development and[name].[chunkhash].js
in production.
Retrieving Filenames from Webpack Compilation Stats
In a development environment, you simply need to include your files in HTML.
<script src="main.js"></srcipt>
However, in a production environment, we get a different filename every time:
<script src="main.12312123t234.js"></srcipt>
To include the correct files in HTML, we need some build information. This can be extracted from webpack compilation stats using the following plugin.
// webpack.config.js
const path = require("path");
module.exports = {
/*...*/
plugins: [
function () {
this.plugin("done", function (stats) {
require("fs").writeFileSync(
path.join(__dirname, "…", "stats.json"),
JSON.stringify(stats.toJson()),
);
});
},
],
};
Alternatively, you can use these plugins to expose JSON files:
- https://www.npmjs.com/package/webpack-manifest-plugin
- https://www.npmjs.com/package/assets-webpack-plugin
A simple file output by webpack-manifest-plugin
looks like this:
{
"main.js": "main.155567618f4367cd1cb8.js",
"vendor.js": "vendor.c2330c22cd2decb5da5a.js"
}
The next steps depend on your server. Here’s a great example [walk through for Rails-based projects](walk through for Rails-based projects). For Node.js server-side rendering, you can use webpack-isomorphic-tools. If your application doesn’t require server-side rendering, you can directly generate an index.html
. This can be done using these two plugins:
- https://github.com/ampedandwired/html-webpack-plugin
- https://github.com/szrenwei/inline-manifest-webpack-plugin
Deterministic Hashes
To compress the size of generated files, webpack uses IDs instead of names to identify modules. During compilation, IDs are generated, mapped to chunk names, and placed into a JavaScript object called the chunk manifest. It is placed in the entry file to ensure the bundled files work correctly.
This leads to the same problem as before: no matter where a file is modified, even if other parts remain unchanged, the updated entry needs to include the manifest file. This generates a new hash, causing our entry filename to change, preventing us from benefiting from long-term caching.
To solve this problem, we should use https://github.com/diurnalist/chunk-manifest-webpack-plugin, which can extract the manifest file into a separate JSON file. Here’s the updated configuration, which will generate chunk-manifest.json
and place it in our bundled folder:
// webpack.config.js
var ChunkManifestPlugin = require("chunk-manifest-webpack-plugin");
module.exports = {
// your config values here
plugins: [
new ChunkManifestPlugin({
filename: "chunk-manifest.json",
manifestVariable: "webpackManifest",
}),
],
};
When we remove the manifest file from the entry file, we then need to manually provide this file for webpack to use. In the example above, you might notice the manifestVariable
option. This is a global variable that webpack uses to find the manifest JSON file, which is why we need to include it in the HTML before our code bundle. Writing the content of the JSON file into HTML is easy, so the <head>
section of our HTML file would look like this:
<html>
<head>
<script>
//<![CDATA[
window.webpackManifest = {
0: "main.3d038f325b02fdee5724.js",
1: "1.c4116058de00860e5aa8.js",
};
//]]>
</script>
</head>
<body></body>
</html>
The final webpack.config.js
file is as follows:
var path = require("path");
var webpack = require("webpack");
var ManifestPlugin = require("webpack-manifest-plugin");
var ChunkManifestPlugin = require("chunk-manifest-webpack-plugin");
var WebpackMd5Hash = require("webpack-md5-hash");
module.exports = {
entry: {
vendor: "./src/vendor.js",
main: "./src/index.js",
},
output: {
path: path.join(__dirname, "build"),
filename: "[name].[chunkhash].js",
chunkFilename: "[name].[chunkhash].js",
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: "vendor",
minChunks: Infinity,
}),
new WebpackMd5Hash(),
new ManifestPlugin(),
new ChunkManifestPlugin({
filename: "chunk-manifest.json",
manifestVariable: "webpackManifest",
}),
],
};
If you are using webpack-html-plugin, you can use inline-manifest-webpack-plugin to do this.
With this configuration, the vendor chunks will not change unless you modify their code or dependencies.
Shimming
Webpack, as a module bundler, supports module systems including ES2015 modules, CommonJS, and AMD. However, in many cases, when using third-party libraries, we see them relying on a global variable like $
or jquery
. They might also create new global variables that need to be exposed. Let’s look at a few different ways to make webpack understand these non-module (broken modules) files.
Prefer unminified CommonJS/AMD files over bundled dist versions.
Most modules specify their dist
version in the main
field of package.json
. While this is very useful for most developers, for webpack, it’s often better to set an alias to their src
directory to allow webpack to optimize dependencies better. However, in many cases, using the dist
version won’t cause major issues.
// webpack.config.js
module.exports = {
...
resolve: {
alias: {
jquery: "jquery/src/jquery"
}
}
};
provide-plugin
By using provide-plugin
, this module becomes available as a variable in all modules referenced by webpack
. The corresponding module is only referenced when you actually use this variable. Many older modules use specific global variables, such as jQuery’s $
or jQuery
. In this scenario, you can pre-configure webpack to var $=require('jquery')
every time it encounters the global $
identifier.
module.exports = {
plugins: [
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery",
}),
],
};
imports-loader
imports-loader
injects necessary global variables into traditional modules. For example, some traditional modules rely on this
pointing to the window
object. This causes a problem when the module is executed in a CommonJS context, where this
points to module.exports
. In this case, you can override this
using imports-loader
.
webpack.config.js
module.exports = {
module: {
rules: [
{
test: require.resolve("some-module"),
use: "imports-loader?this=>window",
},
],
},
};
It supports different module types, such as AMD, CommonJS, and also traditional modules. However, it often checks for the define
variable and then uses some quirky code to expose these properties. In this case, setting define = false
to force the CommonJS path might be helpful.
webpack.config.js
module.exports = {
module: {
rules: [
{
test: require.resolve("some-module"),
use: "imports-loader?define=>false",
},
],
},
};
exports-loader
Suppose a library file creates a global variable and expects its consumers to use it. In this case, we should use exports-loader
to expose a CommonJS-style global variable. For example, to expose file
as file
, and helpers.parse
as parse
:
webpack.config.js
module.exports = {
module: {
rules: [
{
test: require.resolve("some-module"),
use: "exports-loader?file,parse=helpers.parse",
// adds below code the the file's source:
// exports["file"] = file;
// exports["parse"] = helpers.parse;
},
],
},
};
script-loader
script-loader
parses code in the global context, just as if you added a script
tag in HTML. In this case, theoretically, all modules should run normally.
This file will be bundled as a string in the code. It will not be compressed by
webpack
, so please use the compressed version. Also, webpack’s development tools cannot be used in this situation.
Suppose you have a legacy.js
file containing:
GLOBAL_CONFIG = {};
Using script-loader
require("script-loader!legacy.js");
Generally, you will get this result:
eval("GLOBAL_CONFIG = {}");
noParse
Option
When there are no AMD/CommonJS style modules, and you need to include them from dist
, you can mark this module as noParse
. This way, webpack
will only include this module but will not process it, which can also reduce build time.
Anything that requires AST support, such as
ProvidePlugin
, will not work.
module.exports = {
module: {
noParse: /jquery|backbone/,
},
};
Authoring a Library
Webpack is a tool that can be used to bundle application code, and it can also be used to bundle library code. If you are a JavaScript library author looking to streamline the code bundling process, then this section will be very helpful to you.
Author a Library
Here’s a simplified wrapper library to convert numbers 1 to 5 to their corresponding words, and vice versa. It might look like this:
src/index.js
import _ from "lodash";
import numRef from "./ref.json";
export function numToWord(num) {
return _.reduce(
numRef,
(accum, ref) => {
return ref.num === num ? ref.word : accum;
},
"",
);
}
export function wordToNum(word) {
return _.reduce(
numRef,
(accum, ref) => {
return ref.word === word && word.toLowerCase() ? ref.num : accum;
},
-1,
);
}
The library usage specification is as follows:
//ES2015modules
import*aswebpackNumbersfrom'webpack-numbers';
...
webpackNumbers.wordToNum('Two')//outputis2
...
//CommonJSmodules
varwebpackNumbers=require('webpack-numbers');
...
webpackNumbers.numToWord(3);//outputisThree
...
//Asascripttag
<html>
...
<scriptsrc="https://unpkg.com/webpack-numbers"></script>
<script>
...
/*webpackNumbersisavailableasaglobalvariable*/
webpackNumbers.wordToNum('Five')//outputis5
...
</script>
</html>
The complete library configuration and code are available here: webpack-library-example.
Configuring webpack
The next step is to bundle this library:
• Do not bundle lodash
, but expect it to be imported by its consumers.
• Name the library webpack-numbers
, and the variable webpackNumbers
.
• The library can be imported via import webpackNumbers from 'webpack-numbers'
or require('webpack-numbers')
.
• When imported via a script
tag, it can be accessed through the global variable webpackNumbers
.
• It can be used in Node.js.
Adding webpack
Add basic webpack configuration.
webpack.config.js
module.exports = {
entry: "./src/index.js",
output: {
path: "./dist",
filename: "webpack-numbers.js",
},
};
This adds a basic configuration to bundle the library.
Adding Loaders
However, it won’t work without the corresponding loaders to parse the code. Here, we add json-loader
to enable parsing of JSON files.
webpack.config.js
module.exports = {
// ...
module: {
rules: [{
test: /.json$/,
use: 'json-loader'
}]
}
};
Adding externals
Now, if you run the webpack
command, you’ll find a significantly larger code bundle generated. If you inspect the code, you’ll find that lodash
is bundled into the package. For your library, bundling lodash
together is completely unnecessary.
This can be configured via externals
:
webpack.config.js
module.exports = {
...
externals: {
"lodash": {
commonjs: "lodash",
commonjs2: "lodash",
amd: "lodash",
root: "_"
}
}
...
};
This means your library will expect lodash
to be a dependency in the consumer’s environment.
Adding libraryTarget
For this library to be widely used, we need it to behave consistently across different environments. For example, CommonJS, AMD, Node.js, or as a global variable.
To achieve this, you need to add the library
property to the webpack configuration.
webpack.config.js
module.exports = {
...
output: {
...
library: 'webpackNumbers'
}
...
};
This allows your library to be accessed as a global variable when imported. To use it in other scenarios, continue adding libraryTarget
values to the configuration:
webpack.config.js
module.exports = {
...
output: {
...
library: 'webpackNumbers',
libraryTarget:'umd' // Possible value - amd, commonjs, commonjs2, commonjs-module, this, var
}
...
};
If library
is set but libraryTarget
is not configured, libraryTarget
defaults to var
as specified in the config reference.
Final Step
- Adjust the production environment configuration
- Add the bundled file to the specified field in
package.json
.
package.json
{
...
"main": "dist/webpack-numbers.js",
"module": "src/index.js", // To add as standard module as per https://github.com/dherman/defense-of-dot-js/blob/master/proposal.md#typical-usage
...
}
Now you can publish it as an npm module and distribute it to your users via unpkg.com.
This article was published on January 19, 2017 and last updated on January 19, 2017, 3182 days ago. The content may be outdated.