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.