←Home Archive Tags About

Optimizing Webpack build times and improving caching with DLL bundles

Feb 6, 2016 · 5-minute read web-development webpack

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:

  1. It exposes a require() function via a global variable on the page which other bundles can use to require code from that bundle.

  2. 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.

© Copyright 2024 Robert Knight

Powered by Hugo Theme By nodejh