Small Bundles, Fast Pages: What To Do With Too Much JavaScript

Ben Schwarz

Ben Schwarz

September 16, 2021

Illustrated by

 Jeffrey Phillips

Minimising the amount of JavaScript in your pages is an essential step to ensure a speedy user experience.

This post will explain why bundle size matters and recommend tools and processes you can follow to monitor, visualise, and most importantly, shrink your JS bundles.

How does bundle size affect performance?

Large amounts of JavaScript negatively affect site speed in two distinct phases:

  1. During page load: big bundles take longer to download.
  2. During parse and compile: big bundles take longer to be turned into machine code, which delays JS initialisation.

If your users happen to be on a slow, spotty network, a device with a low battery or even just an underpowered Android, large bundle size will likely cause delays during load, render, user interaction or even page scroll.

Of course, your users don’t have to be using old devices or slow networks to have a sub-par experience. The effects of a large bundle can be partially mitigated by caching, compressing and minifying script resources, though reducing the size of a bundle is the only way to guarantee a fast page.

By keeping pages as light as possible, you’re ensuring that every visitor has the best chance of a great experience.

Which performance metrics are affected by bundle size?

In short, most of them! Pages with large amounts of script can delay Largest Contentful Paint, cause Cumulative Layout Shifts, increase First Input Delay, Total Blocking Time and Time to Interactive.

Slow readings in those metrics quantify poor user experiences and can result in SEO ranking penalties.

How much JavaScript is too much?

When we’re talking about performance, we usually focus on the compressed size of resources. However, once resources are uncompressed, they will be 2–3x larger.

For example, a page with 300kB of compressed script can yield 900kB–1.3MB once decompressed.

Here on NPMJS.com, commons.js is 306kB over the wire, but 1.2MB once uncompressed.
Here on NPMJS.com, commons.js is 306kB over the wire, but 1.2MB once uncompressed.

For CPU-constrained devices, multi-megabyte payloads are particularly damaging to performance:

Cost of JavaScript in 2019
Cost of JavaScript in 2019. Used with permission from Addy Osmani.

We recommend restricting pages to a maximum of < 300kb of script (compressed). Where possible, use code splitting to break up your code into small chunks of 50KB or less. That way, browsers can download JS resources in parallel, making full use of HTTP 2 multiplexing.


The new global baseline leaves space for ~100KiB (gzipped) of HTML/CSS/fonts and 300-350KiB of JavaScript on the wire (compressed).

Alex Russell

Tools and automations to keep your code fast

Setup your editor for success

Use the import cost plugin in Sublime or VSCode to report the size of third party libraries as you code:

Import cost plugin in Sublime or VSCode reporting the size of third-party libraries as you code.

With import cost, you can set thresholds for what is considered a small or medium package. We recommend setting more aggressive targets than the defaults:

1// Upper size limit, in KB, that will count a package as a small package
2"importCost.smallPackageSize": 15,
3
4 // Upper size limit, in KB, that will count a package as a medium package
5"importCost.mediumPackageSize": 50,
Tip
Import cost can’t calculate cost-savings of two libraries with a common dependency within bundled code.

Visualise what your bundles include

Use tools like Bundle Buddy, source-map-explorer and webpack-bundle-analyzer to generate interactive bundle treemaps.

In a treemap, larger blocks correlate to larger file sizes—perfect for quickly spotting large imports!

This bundle indicates that SVG icons are included within 0.js.
This bundle indicates that SVG icons are included within 0.js.

By visually exploring bundles, you will be able to identify modules that are larger than expected.

Look for smaller, alternative third-party libraries

Often we choose a library dependency, then use it forever. But, there may be more lightweight alternatives that you’re unaware of.

Using BundlePhobia.com, you can scan a project’s package.json file or search for a given npm package.

Moment.js has increased in size by 15% in the last 15 releases.
Moment.js has increased in size by 15% in the last 15 releases.

When a library is “tree shakable”, bundler tools like webpack, rollup, or esbuild can perform unused code elimination during a build. Opt to use tree shakable libraries when you can!

Bundlephobia suggests luxon, dayjs and date-fns as alternatives to moment.js.
Bundlephobia suggests luxon, dayjs and date-fns as alternatives to moment.js.
Tip
Sometimes libraries are smaller because they don’t support older browsers. Be sure to test thoroughly for edge cases!

Block selected packages from being used

Communicating why to use one package over another can be difficult across teams or companies. To counter this, you can use ESLint’s no-restricted-import to warn or error when a restricted package is included.

In the following example, ESLint will fail the build when we use the moment package, suggesting date-fns as a vetted alternative:

1{
2 "rules": {
3 "no-restricted-imports": [
4 "error",
5 {
6 "paths": [
7 {
8 "name": "moment",
9 "message": "Use date-fns instead. See https://bundlephobia.com/package/moment"
10 }
11 ]
12 }
13 ]
14 }
15}

Dynamically load components and dependencies

Most popular bundlers like Webpack, ESBuild, Rollup or Parcel can code-split your code and dependencies. Code-splitting allows you to lazy-load parts of your application as required, resulting in smaller bundle sizes and faster initial load experiences.

React, Next, Angular, and Vue all provide utilities to make lazily-loading components more straightforward. Here’s a React example:

1import React, { Fragment, Suspense } from 'react'
2import Skeleton from './Skeleton'
3
4// Lazy loading React import
5const Dashboard = React.lazy(() => import('./Dashboard'))
6
7function Page() {
8 return (
9 <Fragment>
10 <Suspense fallback={<Skeleton message="Loading" />}>
11 <Dashboard />
12 </Suspense>
13 </Fragment>
14 )
15}

Lazy-loading comes with many benefits:

  • Less initial script to load
  • A greater number of smaller requests loaded in parallel
  • Code that isn’t changed regularly can be cached long-term

Lazy-loading works well for:

  • Route/navigation based lazy-loading: split the required script required for each page.
  • Interaction based lazy-loading: load dependencies as they’re required. e.g.: when a viewer opens a panel.

Prefer server-side rendering for primary content

Whether for end-users or SEO spiders, we must render primary content as quickly as possible.

For content-driven pages, we recommend server-side rendered (SSR) over Single Page Applications (SPA). Single Page Applications are suitable for applications with long session times or interfaces that seamlessly transition (e.g. shopping carts), but at the same time, we must show content fast. Render server-side when you can.

Lazy load third party resources with facades

Business requirements often drive the usage of third parties, but that does not mean that developers can’t influence third party performance.

At Calibre, we improved Time to Interactive by 30% using react-live-chat-loader, our facade library for Help Scout, Intercom, Facebook Messenger, Drift, Userlike and Chatwoot.

Facade libraries work based on delaying the load of a third party by temporarily showing a ‘fake’ (non-interactive) chat widget, video panel, or support tool until the page has finalised loading critical content.

As a team, you can use several strategies to wrangle third-party performance. Here are some of our favourites:

  • Delay third parties from loading until required using facades.
  • Use dns-prefetching for third party domains, e.g.: <link rel="dns-prefetch" href="https://fonts.googleapis.com/" />.
  • Prefer to bundle third party libraries yourself, rather than using their CDN.
  • Compare page performance with and without a given third party script. Share the results with people making decisions about third party tooling!
  • Request Performance SLAs in contractual agreements with third parties.

Deliver ES6 modules to up-to-date browsers

Supporting older browsers can hold you back from using new technologies (and their performance benefits!). Still, we need to be careful about abruptly dropping support for legacy technologies, which might result in a lack of access.

Consider splitting your build into two builds:

  • An ES5 build, with browser supports, polyfills and Babel transcoding.
  • An ES2015+ build, utilising async/await, Promises, arrow functions, Map and Set types and Dynamic Imports for lazy-loading.
1<!-- Deliver ES5 code to non-module supporting browsers -->
2<script nomodule src="legacy-support-bundle.js"></script>
3
4<!-- Deliver ES2015+ code to module capable browsers -->
5<script type="module" src="bundle.js"></script>
Resource
For instructions to create es5 & ES2015+ builds using Webpack, see Phil Walton’s excellent post, Deploying ES2015+ Code in Production Today.

Keep monitoring JavaScript size

Optimising bundle size doesn’t end with one housekeeping effort (we wish!). As codebases grow and evolve, we need safeguards to keep JavaScript size in check. Some tools mentioned earlier will be helpful here.

Another recommended strategy is using performance budgets. By setting targets, you create accountability for metrics and how they affect user experience. At Calibre, we create budgets for overall JavaScript size and third party JavaScript to get alerts when we exceed them:

Performance Budget dashboard in Calibre showing JavaScript metrics in and out of budget.

You can also use Lighthouse to set performance budgets and script your own solution!


By using a combination of the above tips and strategies, you can improve user and developer experience. If you have other tips or successful workflows, let us know!

Ben Schwarz

Ben Schwarz

Ben is the founder and CEO of Calibre. He uses his experience in far-reaching Open Source projects and web standards to build tools for a better, more accessible web. Find him on Twitter or LinkedIn.

Become a site speed expert

Join thousands of subscribers keeping up-to-date with web performance news and advice.

The best performance newsletter I’ve come across. Highly recommended.

Addy Osmani

Addy Osmani

Engineering Manager at Google Chrome