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
Library authors, please consider adding a module
entry to your package.json
and start producing an ES module version.
Top comments (6)
Tree shaking is not enough:
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!
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...
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...
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
andconsole.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.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.