Edit, June 13, 2019: What a timing... pika.dev 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 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 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.

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