🔥 Speeding up webpack

Seven 3 second changes to reduce build time by 77%

It’s only so often you can wait 5 whole seconds for your build to apply your color: blue; to color: red; change…

At Onfido, we use webpack as our module bundler. As is always the case given the speed of development, our webpack config grew organically, and the speed of the pipeline was an after-thought.

But there came a tipping point. Eventually, we snapped and decided to get that build time way down.

There are already some great articles that discuss ways to increase build speed. To name just a few:

We followed these articles very closely, and used a lot of their suggestions. But, we also made some changes that haven’t been commonly mentioned anywhere else — so I want to discuss them here.

1. Measure

We started by measuring our performance. This really helps to find your current bottlenecks, and compare your progress as you make changes.

I created the Speed Measure Plugin for webpack, which we used to analyse the performance of our plugins and loaders. This let us focus our search, and work out where the easiest wins were. I’ve talked about SMP a bit already, so won’t go into it in any more detail in this post.

Overall, in a repo containing around 50,000 lines of JS/CSS code, these were our build times:

  • Locally (full build): 1 min, 48 secs
  • Locally (watch mode changes): 6.49 secs
  • Jenkins (full production build): 3 mins, 26 secs

2. Upgrade and Parallelise UglifyJsPlugin

Even a cursory glance at SMP’s output shows that UglifyJsPlugin takes a long time to run. For us it took 65% of the entire build time!

We were originally using webpack 3’s built-in UglifyJsPlugin which is set at version 0.4.6.

However, there’s nothing stopping you from upgrading your UglifyJsPlugin version without upgrading webpack, and just manually importing the plugin instead.

Version 1 of the plugin introduces some performance features — namely parallelisation, and caching. If you’re only uglifying on a build server like Jenkins, then caching doesn’t really help you.

Turning on the parallel flag, however, can save you a lot of time — depending on how beefy your machine is, and how many cores it has. For us, running on i3.4xlarge EC2 instances with 16 vCPUs, this was considerable.

Time saved: 🔥 1 minute on Jenkins

3. Remove Image Loaders for Local Development

Most webpack configs have a rule to handle images, and that rule is normally file-loader followed by image-webpack-loader, or some other similar image loader.

But the only necessary loader here is the file-loader, which actually allows the image to end up in the output directory, and its URI passed to the bundle.

The image-webpack-loader optimises these images, minifying and re-encoding them. For local development, this is quite a lot of unnecessary work.

Time saved: 20 seconds locally

4. Don’t Cache for Production Builds

There are a few ways to cache with webpack — like using cache-loader, HardSourceWebpackPlugin, or the ?cacheLoader babel flag. All of these caching methods have an overhead to boot up. The time saved locally during a re-run is huge, but the initial (cold) run will actually be slower.

Caching on production builds that should be running from scratch each time anyway will just be slowing you down.

Time saved: 15 seconds on Jenkins

5. Remove coffee-loader

We had some legacy code that was written in CoffeeScript. This was never seen as that big a deal before, as the code was rarely touched, and the loader did its job fine.

However, SMP revealed that coffee-loader was taking 1,078 ms on average for each module. Compared with babel-loader’s average of 561 ms, this was an obvious enough opportunity for improvement. Not to mention the benefit of reducing the npm install time, and reducing our dependency count!

Simply transpiling the CoffeeScript files to JS with the coffee CLI, and then manually cleaning them up removed this dependency for us.

Time saved: 10 seconds

6. Remove ExtractTextPlugin for Local Development

Using ExtractTextPlugin splits out part of your bundle into a separate files — most often by splitting out stylesheets into a separate CSS file.

This can speed up the end-user’s experience, but adds overhead into the compilation steps. So again, when running locally, this is unnecessary work. Fortunately, this plugin comes with a simple disable flag!

(note that this is different to DllPlugin which splits out your bundle into a separate file, but does so in an entirely different process — which does massively help with your build speed).

Time saved: 7 seconds locally

7. Use Vanilla css-loader When Possible

Whether you’re using PostCSS, SASS, or any other CSS tool, you likely don’t need it running on all of your stylesheets. For us, we had some old legacy stylesheets, and stylesheets coming from third-party dependencies, which were all vanilla CSS.

These don’t need to run through PostCSS or SASS to compile into CSS — so separating out these into separate loader configs can give you a slight speed boost.

Time saved: 2 seconds

All things said and done, we reduced our build times to the following:

  • Locally (full build, cold) — 1m48s → 58 secs / (-46%)
  • Locally (full build, warm) — 1m48s → 25 secs / (-77%)
  • Locally (watch mode changes) — 6.49s → 1.93 secs / (-70%)
  • Jenkins (full production build)— 3m26s → 1 min, 5 secs / (-61%)

Not too shabby!

Actual photo of our webpack build

But the webpack arts are dark ones, and I’m sure that we’ve missed many good tricks. Keep vigilant, webpackers — and be sure to share anything you think we’ve missed here!