On NPM Packages and Bundle Size Impact
Tags: javascript, npm, webpack, performance
Published: 2019-06-12 20:35:36

---
title: On NPM Packages and Bundle Size Impact
published: true
description: NPM packages take up a lot of the size in our web apps, thus playing a significant role in user experience. We need authors to get their libs up to speed with ES modules so we can start tree shaking them.
tags: javascript, npm, webpack, performance
---

_Edit, June 13, 2019: What a timing... [pika.dev](https://www.pika.dev/cdn) have just been released, which is a CDN for ES modules. Their search engine also reveals which packages does not have an ES module entry, try searching for `moment`._

We've got a bundle size problem, and the [heaviest objects of the universe](https://www.reddit.com/r/ProgrammerHumor/comments/6s0wov/heaviest_objects_in_the_universe/) carry a lot of blame. Here's a quick write up on the matter that I hope can spur some debate.

Emphasis on web app bundle size keeps increasing, which means that a lot of frontend engineers eyes are aimed at a search for things to exclude, tree shake, replace, lazy load, ... from their build output. But there's an elephant in the room, that no-one seems to be talking about: NPM packages and their distribution format.

Some background on tree shaking and ES version in NPM before we dive in.


# Tree Shaking

Tree shaking is one of the key ingredients to keeping your application bundle size to a minimum. It's a mechanism used by bundlers like Webpack to remove unused pieces of code from dependencies. This is something that the bundlers can easily determine for ES modules (i.e. `import`/`export`, also known as Harmony modules), since there can be no side-effects.

It is _not_ supported for CommonJS nor UMD modules. And this is the important piece of information you need.


# ES2015+ in NPM Packages

Most frontend engineers prefer to use modern ES features like ES modules, fat-arrow, spread operator, etc. The same is true for many library authors, especially those who are writing libs for web. This leads to the use of bundlers to produce the output which is published to NPM. And this is where we have a lot of potential for optimization.

Casting a quick glance over some of the [most depended upon packages](https://www.npmjs.com/browse/depended) in NPM reveals that a lot of them are publishing CommonJS modules only. In a big project I'm working on, we've got 1,773 NPM packages in node_modules, just 277 of these refer to an ES module build.


# A Problem Shaping Up

Let's outline the problem here:

- How many NPM dependencies does your app have? Likely a lot.
- Does your app use 100% of the code in those dependencies? Very unlikely.
- Can your bundler tree shake those unused code paths? Unlikely.

This problem is even recognized by the most depended upon package, `lodash`, who's authors publish a specific ES module output as `lodash-es`. This is great, as it allows us to use an optimized build of lodash, which can be tree shaken and wont include unused code in our app build.

But this seems like an afterthought, better solutions are readily available and many popular libs does not offer a ES module build.


# Problem Illustrated

To illustrate the problem outlined above, I've initialized a simple reproduction [here](https://github.com/egeriis/bundler-experiment).

## `math`

`math` is a small library with two exports, `cube` and `square`. I've set up rollup to produce both CJS and ES module output.

## `app`

This contains a small app which is bundled using webpack. It consumes 1 function from `math` and correctly tree shakes the unused export from it's output.

## `node`

A small proof that the output of `math` also works in Node.js-land with `require`.

## Outcome

While this is a very small example, an impact on app bundle size is imminently visible when toggling between CJS and ES module output. 

Production build size with ES module is 1.1kb:

```
            Asset     Size  Chunks             Chunk Names
  bundle.index.js  1.1 KiB       0  [emitted]  index
```

While it's 1.16kb with CJS and no tree shaking:

```
            Asset      Size  Chunks             Chunk Names
  bundle.index.js  1.16 KiB       0  [emitted]  index
```

Negligible difference for this teeny example, but the impact can be significant once you consider all the heavy objects in your `node_modules` folder. 


# Problem Solved

In our example above, we have managed to find a simple solution this problem. Our dependency `math` can be used in both Node.js and bundler-land (and browser land, if you target modern browser), and it's simple to achieve.

## How It Works

If you bundle your app with a bundler that supports tree shaking (Webpack 2+, Rollup, and more), it will automatically resolve your dependencies' ES module if present. Your bundler will look for a `module` entry in a depency's `package.json` file before defaulting to `main`. Take a look at `math`'s `package.json` for an example:

```
{
  "name": "math",
  "version": "1.0.0",
  "main": "index.js",
  "module": "indexEs.js",
  "devDependencies": { ... }
}
```

Pretty simple. `math` has two output destinations, one is a CJS module (`index.js`), another a ES module (`indexEs.js`).

## One Gotcha

I've had a library published for a while, which used this approach, and many users have been confused because it's been best practice to ignore `node_modules` in Webpack for a long time. To utilize tree shaking, Webpack must be able to read dependencies' ES modules, so if you require backwards compatible app build, you should also transpile these dependencies in your app build step. This is good if you prioritize bundle size over build time.


# Call for Action

Library authors, please consider adding a `module` entry to your `package.json` and start producing an ES module version.