DEV Community

Ronni Egeriis Persson
Ronni Egeriis Persson

Posted on

On NPM Packages and Bundle Size Impact

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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": { ... }
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (7)

Collapse
 
thekashey profile image
Anton Korzunov

Tree shaking is not enough:

Collapse
 
egeriis profile image
Ronni Egeriis Persson

It is definitely not perfect. I would argue that the only way to move things forward, is if our dependencies ensure that we can tree shake, then we can start optimizing the mechanics. This is, however, not possible at all with CommonJS modules.

I'll check our your post later, looks super interesting, thanks for sharing!

Collapse
 
thekashey profile image
Anton Korzunov

Both rollup and parcel could tree shake CommonJS (terms and conditions apply).
For a better context just into this loooooong conversation - twitter.com/rich_harris/status/113...

Thread Thread
 
egeriis profile image
Ronni Egeriis Persson

My understanding is that webpack can do this too, but it's experimental as there are often side effects that webpack will not be able to see through code analysis. webpack.js.org/guides/tree-shaking...

Collapse
 
dotnetcoreblog profile image
Jamie

I've seen a lot of folks include a full npm and webpack stack for things which really could have been handled better with vanilla JS. I'm not advocating for one over the other, but I feel like more folks could look at whether they need to use systems like npm in order to build their apps.

Putting that to one side, I really like what you've said here. I've used npm packages in the past which even had debugger and console.log statements in them, so I feel like (as with all languages and frameworks) it's worth library authors taking the time to figure out how to produce a smaller, tighter, faster version of their bundles.

Collapse
 
egeriis profile image
Ronni Egeriis Persson

100% agree. And with more and more focus on possible insecurities of NPM packages, there are added emphasis on picking packages only when they're truly valuable.