🔥 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:
- 0–100 in two seconds — speed up webpack (by gvidon)
- Keep webpack Fast (by Rowan Oulton)
- build performance (on the webpack wiki)
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.
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.
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
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
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
(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!
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!