Trim the fat: tips for keeping bundle size small πŸ‹οΈ

bryce

Bryce Dorn

Posted on May 15, 2022

Trim the fat: tips for keeping bundle size small πŸ‹οΈ

It's easy to add a bunch of npm packages to a project. It's also just as easy to add so many that it takes ages for your bundle to to build, download and execute. In the real world this translates to bad user experience or worse: losing users entirely.

I had some spare time this weekend and did some refactoring of my personal site, getting rid of the packages I didn't need and got the project's bundle from this:

public/index.853702c4.js                        282.07 KB  1.49s
β”œβ”€β”€ /react-dom/cjs/react-dom.production.min.js  257.67 KB   48ms
β”œβ”€β”€ /popmotion/dist/popmotion.es.js              62.27 KB   16ms
β”œβ”€β”€ /popmotion-pose/dist/popmotion-pose.es.js    33.59 KB   66ms
β”œβ”€β”€ /stylefire/dist/stylefire.es.js                 25 KB    7ms
β”œβ”€β”€ /pose-core/dist/pose-core.es.js              21.74 KB    7ms
β”œβ”€β”€ /react-pose/dist/react-pose.es.js            21.67 KB   85ms
β”œβ”€β”€ /@emotion/stylis/dist/stylis.browser.esm.js  19.88 KB    4ms
β”œβ”€β”€ /@popmotion/popcorn/dist/popcorn.es.js       17.37 KB    7ms
β”œβ”€β”€ src/js/legos.js                              16.08 KB  318ms
└── /react-inlinesvg/esm/index.js                14.52 KB  207ms
└── + 79 more assets
Enter fullscreen mode Exit fullscreen mode

To this: ✨

public/index.1d2e670f.js                         53.59 KB  348ms
β”œβ”€β”€ /preact/dist/preact.module.js                31.56 KB   19ms
β”œβ”€β”€ /@ctrl/tinycolor/dist/module/index.js        19.45 KB    5ms
β”œβ”€β”€ /preact/compat/dist/compat.module.js         17.13 KB   18ms
β”œβ”€β”€ /react-meta-tags/lib/meta_tags.js             9.39 KB   64ms
β”œβ”€β”€ /@ctrl/tinycolor/dist/module/format-input.js  7.68 KB    8ms
β”œβ”€β”€ src/js/app.js                                 7.52 KB  139ms
β”œβ”€β”€ /preact/hooks/dist/hooks.module.js            7.25 KB   21ms
β”œβ”€β”€ /@ctrl/tinycolor/dist/module/conversion.js    6.44 KB   76ms
β”œβ”€β”€ /react-meta-tags/lib/utils.js                 5.88 KB    4ms
└── /react-meta-tags/lib/meta_tags_context.js     5.07 KB    3ms
└── + 25 more assets
Enter fullscreen mode Exit fullscreen mode

1. Use smaller libraries βœ‚οΈ

This one only applies to React-based projects, but the simplest way to cut out a sizeable chunk from your bundle is to swap React for Preact. There are guides for doing this process in a few steps, and with the preact-compat compatibility layer chances are you won't notice a difference (except for the significantly smaller bundle size!)

Beyond this, take a hard look at your dependencies and decide if you really need all the features they provide. Even small packages can stack up over time. Tools like bundlephobia are helpful for finding smaller alternatives to a library with a similar API.

But even then, you may still be left with a bunch of packages that you don't necessarily need.

2. Rewrite library-heavy code πŸ—‘

Bye emotion πŸ‘©β€πŸŽ€

After using bundlephobia to replace some libraries and make small changes so things still work I realized there wasn't a good reason why I needed some of them at all. Obviously this is only relevant on a case-by-case basis, but the smallest library to affect your bundle size is no library at all!

For example: I was using emotion to style components, but this was overkill for such a small project. There was no good reason why I needed to keep it, so I just scrapped it for old-fashioned CSS and let the bundler take care of it.

Some logic that relied on props to define a styled component's coloring needed to be rewritten but that was easy with CSS variables. This:

const Brick = styled.div`
  .child-class {
    background: ${props => darken(0.08, props.color)};
  }
`;

<Brick color="#fff">
  {children}
</Brick>
Enter fullscreen mode Exit fullscreen mode

Which used both @emotion/styled and polished, was rewritten to use a much smaller color utility library:

const color = new TinyColor(props.color).darken(80).toString();

const cssVars = {
  '--color-1': color
};

<div style={cssVars} className="brick">
  {children}
</div>
Enter fullscreen mode Exit fullscreen mode

Combined with some CSS:

.brick .child-class {
  background: var(--color-1);
}
Enter fullscreen mode Exit fullscreen mode

And the resulting behavior is identical! Removing emotion shrank the bundle significantly. The next biggest one would be getting rid of the library that was added to handle animations.

Animation library go poof πŸ’¨

Framer Motion (previously react-pose) is a powerful animation library. But in my case, too powerful. I added it to play around with moving elements around but it was blowing up the project's bundle for just some simple entry animations.

I ended up replacing the motion component with a class to apply a CSS transform then a useEffect to remove the class after a delay. The new behavior very closely resembles what was before, and it's definitely close enough to rationalize removing such a massive dependency (almost 100kb alone!).

3. Always tree-shake 🌳

Tree shaking is not a new concept and all modern bundlers support it. The simplest example is instead of importing an entire massive library like lodash:

import lodash from 'lodash';

const number = lodash.random(0, 10); 
Enter fullscreen mode Exit fullscreen mode

Use a tree-shakeable library that lets you only import what you want:

import random from 'lodash-es/random';

const number = random(0, 10); 
Enter fullscreen mode Exit fullscreen mode

That way your bundler can ignore the unused portions of a library and only include what's needed. Not every library supports this however; it's wise to seek out the ones that do.

Analyze bundles frequently πŸ”

It's always good to keep track of these things over time so performance doesn't slide. Parcel, which I used for this project, has a helpful bundle analyzer (similar to the one for Webpack) that gives a nice visual overview of a project's bundle. This is especially helpful for identifying bundled dead code coming from packages that could be avoided with tree-shaking. There are also plenty of tools you can integrate with CI to enforce bundle size.

End result ⚑️

This project now takes less than a second to build and the gzipped bundle size is down from ~150kb to only 18kb! The page loads significantly faster and the dev experience is much smoother too.

Hopefully these basic concepts are helpful, please share any tips I didn't cover!

πŸ’– πŸ’ͺ πŸ™… 🚩
bryce
Bryce Dorn

Posted on May 15, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related