Optimizing your webpack builds

Angel M De Miguel

Webpack is the new must-have tool for Frontend development. Frameworks like Angular, React and VueJS use it in their examples and even in their CLIs. But, what's the secret of Webpack? It offers a lot of common and complex features through a single configuration file. Some of them are:

Hot module reloading for development

"Compilation" of the assets

Processing files with different loaders based on the file extension

Plugins

Optimization for production environments

However, the configuration file is both the solution and the most painful aspect of Webpack. Changing one line of the file may break your compilation process and the debug information isn't often too useful. If we combine this with the hundreds of NPM libraries we need to add to our project, we usually get a long compilation process. And you know, time is money.

Find the problem

This is the first step to optimize our Webpack build process. Webpack provides a good tool to analyze the profile of our build. We can get this profile using the Webpack CLI.

Before optimizing it, my build process spent ~20s and compiled 944 modules into 1 chunk file. That means Webpack has to process 944 modules in every compilation. But, where do all those modules come from? Let's move to the modules section to see more details.

This graph represents the relationships between the required modules and the source code. As I mentioned, Webpack needs to process this whole tree to build our project now. In this mess, we can find some libraries with a huge number of related dependencies. You can identify them because they are like black holes, they attract a lot of nodes to them.

What do you think those nodes are? In my example, the black holes are:

In most cases, those nodes are the entrypoints of the libraries you are using in the project. So, why we need to include them in our build process if they don't change? That's a key point to improving our builds, we don't need to recompile the libraries.

Extract libraries from the project build

Webpack Dynamic Linked Library plugins are the solution to our problem. These plugins allow us to extract the libraries that rarely change and reference them in our project instead of building them every time. To accomplish it, we should configure and use two plugins:

DllPlugin: Export all required libraries in the entrypoint as a single bundle javascript file. Also, it exports a private manifest file with the references of the modules in the bundle.

DllReferencePlugin: It loads the libraries in the manifest file from DllPlugin and references them in our source code.

As you may have noticed, now we are going to split our build process in two steps:

Dll build: Bundle all external libraries. We only need to do it every time we include or modify an external library.

Adding or modifying external libraries is not a common action, we will only need to rebuild them a few times. However, you build your project code a lot, so removing the libraries from the project build is a win!

Create the Dll bundle

The first thing we should do is to create a file that references all the libraries of the project. We can call it vendor.js and require there all libraries:

Build the libraries

To build the bundle, we create a new webpack.dll.js file with the following configuration. Remember to customize the paths for your environment:

varpath=require('path'),webpack=require('webpack'),// Bind join to the current path. You can change it:// path.join.bind(path, __dirname, 'app').join=path.join.bind(path,__dirname);module.exports={entry:{// The entrypoint is our vendor filevendor:[join('vendor','vendor.js')]},output:{// The bundle filepath:join('build'),filename:'[name].js',library:'[name]'},plugins:[newwebpack.DllPlugin({// The manifest we will use to reference the librariespath:join('vendor','[name]-manifest.json'),name:'[name]',context:join('vendor')})],resolve:{root:join('vendor'),modulesDirectories:['node_modules']}}

Now, just run Webpack with this configuration file.

webpack --config webpack.dll.js

With this configuration, Webpack will create the two files we need for our project:

build/vendor.js

vendor/vendor-manifest.json

We will build the libraries only the first time or when some of them have changed.

Reference it in project build

After creating the bundle, we should reference it in our project. This is as simple as adding a new plugin into the plugins array of our webpack.config.js file:

varpath=require('path'),webpack=require('webpack'),// Bind join to the current path. You can change it:// path.join.bind(path, __dirname, 'app').join=path.join.bind(path,__dirname);module.exports={// Your config...plugins:[// Your plugins...newwebpack.DllReferencePlugin({// An absolute path of your application source codecontext:path.join(__dirname,"app"),// The path to the generated vendor-manifest filemanifest:require(path.join(__dirname,"./vendor/vendor-manifest.json"))}),]}

This plugin only loads the references, but we must include the vendor.js file in the generated HTML of our project.

<script src="vendor.js"></script>

Compare results

This is the analysis with our new awesome Webpack configuration.

We have reduced the build time from ~20s to ~5s and the number of modules from 944 to 220. This optimization affects hot rebuilds too. Before the optimization, Webpack was taking 4-5s to rebuild the source code after a change. Now, it only takes 250-500ms!

As you can see, the modules graph doesn't have any black holes now.

Other tips to improve the performance

Extracting libraries from the project build is the most important optimization. Nevertheless, there are other good practices to improve the build/rebuild time.

Prefetching

The Webpack analysis tool has a section called Hints. If you navigate to this section, you may read that you can "use prefetching to increase build performance". Prefetch is another Webpack plugin. It preloads files before starting the build. So, we can add project modules with a lot of dependencies before starting the build. Add a PrefetchPlugin for each one of them to the plugins array of your application:

Source maps

Source maps can be very heavy and expensive to create, but we need them for debugging in development. Webpack has several approaches to generate these files. For development, I recommend you use cheap-module-eval-source-map, because it's very fast and it maps the generated code with the source code. Other source map strategies don't provide the original source code. There is a good comparison from the Webpack documentation.

Summary

Optimizing Webpack is not trivial. I spent a few hours reading articles and documentation. But, it's important for developers to have a properly environment. Before the optimization, we wasted minutes waiting for the build processes. Now, we can develop the project faster.