Reducing JavaScript bundle size

Alan Casagrande
OnFrontiers Engineering
4 min readNov 14, 2020

As we write more and more JavaScript in order to deliver better user experience, we should be aware of the impact that have on users. More code means more bytes to download, which ultimately increases the time users wait to start navigating.

In this article we will use an app with minimal settings to demonstrate some of the tools and basic techniques we use here at OnFrontiers to reduce bundle size and keep initial load time short, by optimizing the build incrementally.

We will use Webpack to bundle the source code and run the bundle analyzer, and also run a small Node server to serve our app.

The full code is available at http://github.com/OnFrontiers/bundle-size-demo

Project structure

Run the demo app

  • Install dependencies: npm install
  • Build the app: npm run build
  • Start the server: npm start
http://localhost:8080

Webpack Bundle Analyzer

With this plugin we can visualize how big is our bundle and what dependencies are impacting the most. It can be pretty useful to identify parts of the bundle to optimize and decide what strategy to use (and possibly find unnecessary dependencies).

The plugin is already to our Webpack configuration and should be available at http://localhost:8888:

http://localhost:8888

Vendor Chunk

This is one of the most common ways to use code splitting. For a more detailed explanation, check out https://webpack.js.org/guides/code-splitting

Let’s say we need to use code from a library such as lodash. Let's add it and see what happens to our production build:

npm install --save lodash

You can see lodash is now part of the index bundle and responsible for the huge increase on its size.

Once added, we rarely need to update this dependency. However, because everything is in a single bundle, the user has to download lodash again whenever we release a new version of our app, even if lodash has not changed.

If we extract lodash to a separate bundle, then it can be cached by the browser and users only need to download what actually changed (the code we control).

Webpack 4 comes with the splitChunks optimization and the defaultVendors option, which extracts modules from node_modules and move them to a separate bundle.

Remember to add the new bundle to index.html:

We can see 2 bundles now, vendors and index.

Note that although subsequent visits have better performance, the first load time did not improve. The download size is the same and now it requires 2 HTTP requests.

Let’s see how we can improve that.

Tree Shaking

Tree Shaking is a form of dead-code elimination.

You might have noticed that we import the whole lodash library but only use the join function. We can optimize that by importing only what we use, and Webpack will eliminate all unused code from the bundle.

We can achieve that by simply changing the import syntax from:

import _ from "lodash"

to

import join from "lodash/join"

Be aware of the syntax above. It is not equivalent to named imports (for the purpose of tree shaking):

import { join } from "lodash"

With the named import we’re still importing the whole library.

Our bundle is way smaller now:

Dynamic Import

Dynamic Import is another form of code splitting.

Let’s say lodash is only used upon user interaction that rarely occurs. We can optimize that by downloading lodash only when it is going to be used.

webpackChunkName is a directive that defines the bundle name

We can see lodash is still in a separate bundle:

But it is not loaded initially:

But when we click the button:

Moment.js bundle size

If you use moment, you may notice that it has a huge size due to all the bundled locations that you might not need.

To exclude those from the bundle you can use the Webpack IgnorePlugin:

Conclusion

Hopefully these simple techniques will form the basis for more complex scenarios and help you to improve user experience by reducing waiting time.

One common use case that can be explored in a future article is to have separate bundles based on app routes, such as /admin.

--

--