Trim the fat: tips for keeping bundle size small 🏋️
Tags: javascript, react, beginners, webdev
Published: 2022-05-15 20:20:32

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](https://bryce.io/), getting rid of the packages I didn't need and got the project's bundle from this:

```java
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
```

To this: ✨

```java
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
```

## 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](https://preactjs.com/). There are [guides](https://preactjs.com/guide/v10/switching-to-preact/) 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](https://bundlephobia.com/) 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](https://emotion.sh/) 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](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties). This:

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

<Brick color="#fff">
  {children}
</Brick>
```

Which used both [@emotion/styled](https://www.npmjs.com/package/@emotion/styled) and [polished](https://polished.js.org/docs/), was rewritten to use a much smaller color utility library:

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

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

<div style={cssVars} className="brick">
  {children}
</div>
```

Combined with some CSS:

```css
.brick .child-class {
  background: var(--color-1);
}
```

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](https://www.framer.com/motion/) (previously [react-pose](https://popmotion.io/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](https://webpack.js.org/guides/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:

```js
import lodash from 'lodash';

const number = lodash.random(0, 10); 
```

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

```js
import random from 'lodash-es/random';

const number = random(0, 10); 
```

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](https://parceljs.org/features/production/#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](https://github.com/ai/size-limit) 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!