DebugBear Blog
23 Oct 2018

Keeping your JavaScript bundle size in check

It's easy to keep adding new packages and lose track of the size of your Webpack, Parcel, or Rollup bundle. But large bundles slow down your page not just because they take longer to download, but also because of an increase in parse and execution time during initialization.

This article outlines some tips and strategies to avoid these performance problems by optimizing your bundle size.

Consider bundle size when adding new dependencies

Whenever you add a new package, ask yourself how much code this will add. Libraries that seem small can end up pulling their own dependencies and actually be quite big.

There are some tools that can help you here. For example, the Import Cost plugin for Visual Studio Code will show you the size of the imported package when you add it:

Visual Studio Code Import Cost plugin

Another useful tool is BundlePhobia, a website that provides more detailed bundle size information. You can also upload your package.json file to view your largest dependencies.

Visual Studio Code Import Cost plugin

Analyze your bundle to find out what packages contribute the most code

There's a great Webpack plugin called Webpack Bundle Analyzer that will visualize your bundles as a treemap.

Unlike the other tools mentioned so far this will also include all of your own code.

Webpack Bundle Analyzer

Compile your bundle just for the browsers you need to support

Compiling your code with Babel is needed to support older browsers, but it also makes your code much more verbose.

Let's take this ES6 one-liner as an example:

const doubleArray = arr => [...arr, ...arr]

After compilation it turns into this ES5 code:

function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }

var doubleArray = function doubleArray(arr) {
  return [].concat(_toConsumableArray(arr), _toConsumableArray(arr));

That's because many browsers don't support the more compact arrow functions or the ... spread operator.

However, if you only need to support the latest versions of Chrome, Firefox, and Edge, then the whole transformation is unnecessary.

This is where babel-preset-env is useful. You tell it what browsers you need to support and it will automatically enable the right Babel transformations.

Use code splitting

When a user loads your app, chances are they are only going to use a few pages and features. You don't need to load your entire front-end code.

Code splitting allows you to split your bundle into just the bits that are needed for each case. Instead of generating one large bundle, you generate a small bundle that contains all the essential feature code.

Then, when the user wants to use a more complex feature, that code is only downloaded when needed, for example after a user clicks on a button. (You can also prefetch extra bundles once your page has rendered.)

Webpack can also make sure that your dependencies are bundled together if they are required by multiple bundles. That avoids fetching the same package several times.

Manually look at your bundle

This technique is surprisingly effective. Just look at your compiled bundle file and see what's in it. Is there code or data that's repeated many times? Is there code you didn't expect to be there?

For example, one common issue you might discover is that you're including all Moment.js locales.

Dead code elimination with environment variables

When the minifier sees code like this it knows it can remove it, because it will never run:

if (false) { /* Do some stuff */ }

That's called dead code elimination. Now, normally you won't write code and then hard-code it to never run, but you might only need certain code in your development environment. In that case you can do this:

if (process.env.NODE_ENV === "development") { /* Do some stuff */ }

Normally process.env is only available in Node, not in the browser. But Webpack will replace it with the value provided in the mode configuration property. Then, depending on the value that property, the code will be removed during minification.

In addition to NODE_ENV you can also add your own environment values, for example using Webpack's environment plugin.

Make sure your code takes advantage of tree shaking

Tree shaking allows you to load only the parts of a package you need, rather than the whole package.

If you write _ = require("lodash") the whole of Lodash needs to be included in your bundle. But if you write import { find, throttle } from "lodash" Webpack can identify just the necessary code.

In the case of Lodash you can also use babel-plugin-lodash to automatically select the necessary exported functions.

Monitor your website and bundle

You won't always notice if a dependency you just added is bigger than expected – so you want to monitor your bundle and get alerts.

Ideally this should be part of CI. You can either build something yourself or use a service like BundleCop. Then your pull requests will include information on bundle size changes.

DebugBear can also show you how your bundle size changes over time. While the feedback won't be instant you'll still be notified if there's a big change.

Want to monitor your website's performance and Lighthouse scores? Learn more.

Get new articles on web performance and debugging by email.