One of my first tasks when I started at Vibbio was to take a look at our frontend webpack bundle size and see if there were any wins to be had. Sure enough, I found some quick and easy repeatable steps that anyone could use to reduce an inflated bundle size.

We started out with a bundle size of 3.88mb (minified and gzipped). Not ideal, not ideal at all. The first step was finding out where the problems lay.

Bundle analysis with webpack-bundle-analyzer

To that end, the first thing I did was to hook up webpack-bundle-analyzer. This is as simple as installing it with npm i -D webpack-bundle-analyzer and adding it to the list of plugins in the webpack config:

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}

In practice, you don’t always want this plugin running. It’s going to slow your build down, and it starts an HTTP server to serve up the analysis every time you run a build. To that end, I put it behind a command-line argument, --analyze, so that I could easily pull it up again whenever I want to analyze a build:

plugins: [
  ...(process.argv.indexOf('--analyze') !== -1 ? [new BundleAnalyzerPlugin()] : [])
]

Once you have this up and running, just run your webpack build as usual, and webpack-bundle-analyzer will open a webpage for you with a visualization of your bundle’s module breakdown. It will look something like this:

webpack-bundle-analyzer analysis visualization

Hey there, wait a second, what the hell is that?!

Moment of truth

everything is on fire

This is the moment you discover what all those heavy bundle megabytes were really spent on.

In this case, an innocent enough looking module, react-password-strength, was taking up over 395kb (minified and gzipped) of the bundle. That’s over 10% of the bundle size! In fact, it’s more than our more than our entire codebase.

Other honorable (hall of shame) mentions go out to:

  • lodash which was being included in the bundle twice.
  • moment which included all its translation data, even though none of it was ever used.
  • react-burger-menu which included the gigant snapsvg library for an optional feature… which we weren’t using.

The good news is that you now have all of these sources bloat now staring you in the face, and a great visualization that will instantly gratify you for cutting them out.

Get some easy wins

Here are the steps you can take to get your easy wins:

Re-evaluate if you really need that module

This is the most obvious question to ask. Do you really need that password strength indicator? Is it worth increasing your bundle size by 10%? For us, the answer was no. We simply cut it out altogether, and in a few seconds our bundle size was cut by almost 400kb. That is an easy win.

Check the alternatives

If you decide you need the module, you can check to see if there are any alternative lightweight modules that provide the same functionality. This was the case for us with moment. It turns out inside our app, we really only needed moment’s format. moment didn’t provide a convenient way for us to just import that function, so we found the extremely impressive date-fns and have never looked back.

Directly import the functionality you need

This ended up being the biggest winner for us. Instead of importing an entire module to get one small function, like this:

import { isEqual } from 'lodash'

Import that function directly from its source, and nothing else. Some modules make this easy for you:

import isEqual from 'lodash/isEqual'
import format from 'date-fns/format'

Some modules will require you to dive into their source tree to find the bit you want. This can be a bit more temperamental, as the module author could change their source tree—but you will find out pretty quickly: your build will break.

import Menu from 'react-burger-menu/lib/menus/slide'

The good news is that this step will cut entire subdependencies out of the picture—such was the case with react-burger-menu. Directly importing the one function we needed meant that snapsvg was no longer in the bundle at all.

I actually thought that webpack would have been doing a lot of this automatically with “tree shaking”, but apparently that’s not the case. Using direct imports like this saved as a huge amount on bundle size.

Celebrate… conservatively

mild celebration

All told, after going through and applying as many of these easy wins as I could, I decreased our bundle size by 54% (3.88mb -> 2.11mb). A gigantic improvement, but still far from ideal.

I recommend always going through this process when you need to add a new module to your bundle: Check the size of a new module before adding. Re-evaluate whether you actually need that fancy module. Check if there are any lighter modules out there that can do the same thing for you. Use direct imports of the code you want to use.

If you import modules mindfully, hopefully you will never be in the position of needing to go back and take these steps.