Optimizing Webpack build times and improving caching with DLL bundles
Feb 6, 2016 · 5-minute read
Webpack’s Dll
and DllReference
plugins are a way to split a large JavaScript
project into multiple bundles which can be compiled independently. They can be
used to optimize build times (both full and incremental) and improve caching for
users by putting code which changes infrequently into separate “library”
bundles. The term ‘Dll’ is short for Dynamically Linked Library which is a
feature for native Windows applications that solves the same problem.
For example, suppose you have an application built with several large libraries
or frameworks such as jQuery
, Angular
or React
which change relatively
infrequently, plus some utility code such as underscore
or lodash
which you
change very rarely. The Dll
plugins enable you to put the utility code in one
bundle, the frameworks in another and your application code in a separate one.
This has several benefits:
- Your can compile the library bundles separately from the application bundle.
This means that during typical development, you would compile your library
bundles once and then run Webpack in
--watch
mode to recompile your application bundles as you change your app. Since the application bundle is smaller, the initial build will be faster and incremental builds will usually also be quicker. - When your users load your app, they will only need to download the bundles which have changed.
- You can avoid cluttering up the configuration for your app bundle with plugins/loaders etc. that are only needed for vendor bundles or vice-versa. This helps with both build speed and maintainability.
For common versions of major libraries (eg. jQuery, Angular) you can also get
these benefits by consuming them from a CDN such as cdnjs
using <script>
tags and you should consider doing that.
The advantage of Webpack library bundles is that you can choose how to assign
modules to bundles and it works with libraries that you consume from npm
and
require()
from your app code.
Creating library bundles
To partition your code into library and application bundles, you will need to create two Webpack configurations, one that creates the library bundle(s) and another that creates the application bundle(s) that use code from the libraries.
In the configuration for your library bundle, add DllPlugin
to the list of
plugins. DllPlugin
does two things:
-
It exposes a
require()
function via a global variable on the page which other bundles can use to require code from that bundle. -
It generates a JSON manifest, which is a mapping between the file paths of modules inside the bundle and the integer IDs that each module has been assigned. This manifest is used by your application bundles to determine whether a library bundle provides a particular module and if so, how to access it from another bundle.
// vendor-bundles.webpack.config.js
var webpack = require("webpack");
module.exports = {
entry: {
// create two library bundles, one with jQuery and
// another with Angular and related libraries
jquery: ["jquery"],
angular: ["angular", "angular-router", "angular-sanitize"]
},
output: {
filename: "[name].bundle.js",
path: "dist/",
// The name of the global variable which the library's
// require() function will be assigned to
library: "[name]_lib"
},
plugins: [
new webpack.DllPlugin({
// The path to the manifest file which maps between
// modules included in a bundle and the internal IDs
// within that bundle
path: "dist/[name]-manifest.json",
// The name of the global variable which the library's
// require function has been assigned to. This must match the
// output.library option above
name: "[name]_lib"
})
]
};
The generated bundle will look something like this:
var angular_lib = (function(modules) {
// Bundle bootstrap code...
})([
// Module 0 re-exports the require function,
// so that it becomes the value of angular_lib
function(module, exports, __webpack_require__) {
module.exports = __webpack_require__;
}
]);
After this is loaded, window.angular_lib(<id>)
can be used by any other code
on the page to require code from that bundle. Since modules use integer IDs
which are internal to the bundle, you also need the JSON manifest in order to
map from file path to ID.
The JSON manifest is structured like this:
{
"name": "angular_lib",
"content": {
"./node_modules/angular/index.js": 1,
...
}
}
Creating application bundles
In the configuration for your application bundle, you will add a
DllRefrencePlugin
instance to the list of plugins, one per library that you
want to consume.
DllReferencePlugin
specifies the path of a manifest previously generated by
DllPlugin
to search for modules.
When Webpack encounters a require()
in your code, it will first check the
available DLLs to see if one provides that code. If it does, a stub module
containing a reference to that DLL will be generated. Otherwise, the actual code
of the required module will be included in the application bundle as normal.
// app.webpack.config.js
var webpack = require("webpack");
module.exports = {
entry: {
app: "./src/index"
},
plugins: [
new webpack.DllReferencePlugin({
context: ".",
manifest: require("./dist/jquery-manifest.json")
}),
new webpack.DllReferencePlugin({
context: ".",
manifest: require("./dist/angular-manifest.json")
})
]
};
If you look inside the generated app.bundle.js
, you’ll find code like this:
// in your module
var angular = __webpack_require__(2);
/* 2 */
// get a reference to the bundle containing 'angular',
// then require module (1) from that
module.exports = __webpack_require__(3)(1);
/* 3 */
// re-export the global variable created by the library
// bundle
module.exports = angular_lib;
A minimal complete example can be found in this Gist.
Comparison with other code splitting methods
The DLL plugins are not the only code-splitting mechanism available in Webpack. The other ones are:
-
CommonsChunkPlugin extracts out code that is shared between multiple bundles and puts it into a separate bundle. The advantage is that you don’t need to specify which code to put in the shared bundle, Webpack can automatically identify that.
You can force specific libraries into a ‘commons’ chunk, and this is a good solution if you only have a moderate amount of vendor code.
The advantage is that you only need one Webpack configuration. The disadvantage for large projects is that the ‘commons’ chunk will be recompiled every time you run Webpack.
-
Code splitting via
require.ensure()
allows lazy-loading of chunks of code as particular pages or features are used. This is useful to optimize the initial page load time by keeping the main bundle small and then pulling in code for lesser-used features on-demand.