Get insightful engineering articles delivered directly to your inbox.
By

— 5 minute read

Optimizing Webpack for Faster React Builds

XKCD’s famous compiling comic

If you’ve got a slow Webpack build with a ton of libraries - fear not, there’s a way to increase incremental build speed! Webpack’s DLLPlugin lets you build all of your dependencies into a single file. It’s a great alternative to chunking. The file is later consumed by your main Webpack configuration or even on other projects sharing the same set of dependencies. Your typical React app may contain dozens of vendor libs depending on your flavor of Flux, add-ons, router, and other utilities like lodash. We’ll save precious build time by allowing Webpack to skip over any reference contained within the DLL.

This article assumes you’re already familiar with a typical Webpack and React setup. If not, please checkout SurviveJS‘s excellent section on Webpack and React and come back here when your build time starts to creep upwards.

Step 1, List your Vendors

The easiest way to build and maintain a DLL is by creating a JS file in your project, let’s call it vendors.js and requiring all of the libs you use. For example, on our recent projects, our vendors.js file looks like this:

require("classnames");
require("dom-css");
require("lodash");
require("react");
require("react-addons-update");
require("react-addons-pure-render-mixin");
require("react-dom");
require("react-redux");
require("react-router");
require("redux");
require("redux-saga");
require("redux-simple-router");
require("redux-storage");
require("redux-undo");

This is the file we will “build” into a DLL. It has no functionality, it just imports the libraries we use.

Note: You could also use ES6 style import here, but then we’d need Babel just to build the DLL. You can still use import and all the rest of the ES2015 sugary goodness in your main project just as you’re used to.

Step 2, Build your DLL

Now we can create a Webpack configuration to build the DLL. This is completely separate from your app’s main Webpack configuration and will result in a few files. It won’t be called by your Webpack middleware, Webpack server, or anything else (except manually or through a pre-build step).

Let’s call this file webpack.dll.js

var path = require("path");
var webpack = require("webpack");

module.exports = {
    entry: {
        vendor: [path.join(__dirname, "client", "vendors.js")]
    },
    output: {
        path: path.join(__dirname, "dist", "dll"),
        filename: "dll.[name].js",
        library: "[name]"
    },
    plugins: [
        new webpack.DllPlugin({
            path: path.join(__dirname, "dll", "[name]-manifest.json"),
            name: "[name]",
            context: path.resolve(__dirname, "client")
        }),
        new webpack.optimize.OccurenceOrderPlugin(),
        new webpack.optimize.UglifyJsPlugin()
    ],
    resolve: {
        root: path.resolve(__dirname, "client"),
        modulesDirectories: ["node_modules"]
    }
};

This is a fairly typical Webpack config except for the webpack.DLLPlugin, which contains properties for the name, context, and manifest path. The manifest is very important, it gives other Webpack configurations a map to your already built modules. The context is the root of your client source code and the name is the name of the entry, in this case “vendor”. Go ahead and try running this build with the command webpack --config=webpack.dll.js. You should end up with a dll\vendor-manifest.json that contains a nice little map to your modules as well as a dist\dll\dll.vendor.js which contains a nicely minified package containing all of your vendor libs.

Step 3, Build your Project

Note: The following example does not include sass, assets, nor a hotloader. They should still work just fine if you’ve got them in your config.

Now all we need to do is add the DLLReferencePlugin and point it at our already built DLL. Here’s what your webpack.dev.js might look like:

var path = require("path");
var webpack = require("webpack");

module.exports = {
    cache: true,
    devtool: "eval", //or cheap-module-eval-source-map
    entry: {
        app: path.join(__dirname, "client", "index.js")
    },
    output: {
        path: path.join(__dirname, "dist"),
        filename: "[name].js",
        chunkFilename: "[name].js"
    },
    plugins: [
        //Typically you'd have plenty of other plugins here as well
        new webpack.DllReferencePlugin({
            context: path.join(__dirname, "client"),
            manifest: require("./dll/vendor-manifest.json")
        }),
    ],
    module: {
        loaders: [
            {
                test: /\.jsx?$/,
                loader: "babel",
                include: [
                    path.join(__dirname, "client") //important for performance!
                ],
                query: {
                    cacheDirectory: true, //important for performance
                    plugins: ["transform-regenerator"],
                    presets: ["react", "es2015", "stage-0"]
                }
            }
        ]
    },
    resolve: {
        extensions: ["", ".js", ".jsx"],
        root: path.resolve(__dirname, "client"),
        modulesDirectories: ["node_modules"]
    }
};

We’ve also done a few other things to increase performance including:

  • Make sure we have cache: true
  • Make sure that the babel loader has cacheDirectory:true in the query
  • Used an include for the babel loader (you should do this for all loaders)
  • Set devtool to eval because we’re optimizing for build time #nobugs

Step 4, include the DLL

At this point, you’ve generated a vendor DLL file and you’ve got a Webpack build going to generate your app.js file. You need to serve and include both files in your template, but the DLL should come first. You’ve likely already got a template set up using the HtmlWebpackPlugin. Since we don’t care about hot reloading a DLL, you don’t really need to do anything special except including a <script src="dll/dll.vendor.js"></script> before your main app.js. If you’re using webpack-middleware or your own custom server, you’ll also need to make sure that DLL is being served. At this point, everything should be running as it was, but incremental builds with Webpack should be blazing fast.

Step 5, build scripts

We can use NPM and package.json to add a few simple scripts to take care of building for us. To clean out the dist folder, go ahead and run npm i rimraf --saveDev. Now add to your package.json:

"scripts": {
    "clean": "rimraf dist",
    "build:webpack": "webpack --config config.prod.js",
    "build:dll": "webpack --config config.dll.js",
    "build": "npm run clean && npm run build:dll && npm run build:webpack",
    "watch": "npm run build:dll && webpack --config config.dev.js --watch --progress"
  }

Now you can run npm run watch. If you’d rather run build:dll manually, you can remove it from the watch script for faster startups.

That’s all, folks!

I hope this gives you insight into how InVision uses Webpack’s DLLPlugin to increase our build speed. If you have any thoughts or questions, feel free to leave a comment!

Like what you've been reading? Join us and help create the next generation of prototyping and collaboration tools for product design teams around the world. Check out our open positions.