Reducing JavaScript bundle size
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
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:
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.
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
.