Unpopular Metrics about JS Bundlers

Andrea Giammarchi
9 min readOct 25, 2017

--

original picture from assembledproducts.net

Bundling JavaScript is a de facto best practice for Web development, one of those mandatory production steps every Web site/app/PWA should do.

Why using bundlers?

The general goal of every bundler can be summarized in a sentence: make a generic modules based development environment consumable by targeted browsers.

Modules help developers reuse and share same code across different parts of one or many applications and it’d be silly these days to pollute the global scope with different modules, unless your goal is to create a global library consumable with or without bundlers (sometimes that’s still convenient).

Which metric matters for your bundler?

Usually developers do simple math: does this bundler do everything? I’m sold.

To me, it depends on what is the aim of your project. When it comes to just a library or module development, having “everything” is overkill for various reasons. As example, these days I have poor internet connection and I am also using data-roaming via a MiFi device and a pay as you go SIM card.

I don’t like having npm modules installed globally because these forces an external dependency for eventual projects contributors that might not be welcome. I’m all for clean folders and clean envs as much as possible so having something simple that weights few Ks is, in most case, my best pick.

The bundler size itself

bundlers size once installed

I have taken as comparison the usual three bundlers, and I’ve added my latest attempt to simplify my life as modules author: asbundle.

My bundler does one thing and one thing only: it loads either ESM (ECMAScript Modules) or CJS (CommonJS) files and produces an ES3+ compatible output normalized for each file.

Each ESM is loosely transpiled into CJS and live bindings are lost. I’m OK with it since I don’t need/use live bindings much (it’s also easy to delegate them).

The result is an Esprima based utility that fits in 380KB, as opposite of over 1.3MB for the still contained rollup alternative, up to 16MB+ for webpack.

It’s worth saying that once you install 2 basic rollup plugins such commonjs and node-resolve, its size goes up to 3.6 MB; still reasonable.

TL;DR if bundler size matters to you, don’t use browserify or webpack.

Bundling performance

This cannot be considered really an unpopular opinion, ’cause bundling performance is usually what developers want the most. My take on this is that if you have configured your IDE to bundle+test everything per each Ctrl+S you hit, then this metric is essential for your work.

However, if your project does not need to bundle while developing, and you bundle+test production code only before pushing/publishing your code, then this metric might be almost irrelevant.

In any case, when it comes to very basic modules, all options perform well.

how long does it take to bundle a basic module?

The bundle is the result of a file requiring a module that exports a number.

global.module = require("./module"); as main.js

module.exports = Math.random(); as module.js

The basic testing repository will produce files that should be consumable by browsers right away: after all, what else are we bundling for, if not browsers?

The minimalistic module example uses the global on purpose because that’s the real de-facto convention between Node and Browsers: when you bundle and you have global in it, that means window.

All bundlers produce valid code. You can test it via npm test after building all bundles via npm run all (but don’t forget to npm install first though).

Bundle code and size

Without using any special plugin or tool, you might want to test directly the produced bundle code as it is. Here the possible questions we’d like to answer:

  • is the code preserved as it was written originally so that it’s easy to debug and find 1:1 issues with the source code?
  • is there any undesired global scope leak or pollution?
  • how much extra code is needed to make the bundle work reliably?

The output produced by asbundle is basically 1:1 the original code. The only eventual change is done by making ESM static import/export a CJS operation. Such transformation is granted to work and it should never be on your way while debugging.

Pretty much the same happens to browserify: resulting code is as it was.

Different story for rollup where you can’t tell anymore what’s that you wrote, but we’ll see later this is a strength of the bundler, and similar thoughts for some webpack transformation, but overall code looks preserved.

Back to rollup, if you specify a format as IIFE (Instant Inline Function Execution) you need to specify a bundle name and that will leak in the global scope, in case you’ll include the resulting file as it is on any Web page.

If you specify CJS format instead, there won’t be a function wrap and every module variable will leak. Maybe there is a way to execute the code and avoid global scope pollution, so I might update this paragraph in the near future.

However, rollup produced the smaller bundle size of them all.

This is thanks to its “tree shaking” feature where everything gets optimized in a single closure without even needing to require code at runtime.

To answer the last question, the total amount of churn around source code is:

  • asbundle 344 uncompressed, 176 minified, 152 gzipped
  • browserify 668 uncompressed, 621 minified, 364 gzipped
  • rollup 226 uncompressed, 147 minified, 116 minzipped
  • webpack 3188 uncompressed, 674 minified, 374 gzipped

It’s worth noticing that removing the global assignment, rollup could produce 68 bytes of minified and gzipped final bundle, currently ruined by that variable leak I wish I could drop but … kudos for rollup on final size!

Is the bundle compatible with ECMAScript modules?

Few developers believe that introducing core modules via import and export would make the usage of bundlers meaningless but that’s absolutely not the case. Bundlers are still our best friends here, with the ability in some case to code split our application in various chunks of JavaScript that will be consumed on demand. Nobody needs browsers to perform a network request per each module source code and we still want minifiers to do their job.

Accordingly, while it’s awesome we can develop without dependencies directly through ECMAScript standar modules on most modern browsers, we still want to be sure our bundle is compatible with the present and the future of modules.

browserify, as example, is not there yet. There’s no way I could include it on the chart because it throws when it tries to parse modern modules.

rollup works almost perfectly fine. If you type module=esm npm run all you can see that rollup produces the best file of them all: all dependencies have been put in a single scope and no global leak happens.

The rollup code fails executing though, because it instantly forgets the global convention so that unless you have such variable defined somewhere else, this code won’t work out of the box once bundled.

If you want it to work put <script>var global=this;</script> on top of your Web page and that’s it, rollup code will have no isssues.

What about asbundle and webpack?

The minimal gzipped final size is 269 bytes for asbundle and 445 for webpack. Worth saying that rollup would have produced 102 bytes of code.

Is the bundle compatible with hybrid modules?

For hybrid modules I mean those modules that would not work out of the box on browsers and that loads CJS modules through ESM semantics.

import random from "./module"; as main.js and module.exports = Math.random(); as module.js.

Even if non standard and full of hidden issues, these modules are quite popular and if we use a tool we want it to be compatible with third parts libraries that might have been published as such.

asbundle, rollup, and webpack are fine with it too but rollup will have the same problem with the global variable not usable as it is, even if we interacted with a CommonJS module.

Results in size are very similar, with 214 bytes for asbundle and 422 bytes for the minified and gzipped webpack bundle.

Bundling React

Now that we’ve seen some metric around a bare minimum module, let’s see how things work with one of those popular libraries that is published as CJS, hence compatible with all bundlers of this post.

The main.js file now contains only the following code, aiming to make React available as global library.

global.React = require("react");

To begin, all bundlers create a file that leaks React on the global scope.

The next question is: how long does it take to bundle it?

Since asbundle does very minimal transformation, it’s easy to win the bundling performance, with browserify in the second place.

But how different is the final bundle size?

  • asbundle 72125 uncompressed, 25003 minified, 7944 gzipped
  • browserify 78486 uncompressed, 28157 minified, 8858 gzipped
  • rollup 71454 uncompressed, 24625 minified, 7892 gzipped
  • webpack 81084 uncompressed, 27270 minified, 8606 gzipped

Even if it produced the best bundle, rollup couldn’t resist to create a global leak. I should’ve named the bundling React directly here but I wanted to compare apples to apples via same code which is why the leak exist.

Beside that, considering bundling and results, rollup comes out as the winner for libraries and projects with a source code of about 100K, followed by asbundle when it comes to code size under 8K, with webpack over 8.5K and browserify closer to 9K.

What else to consider?

Each bundler in this post has pros and cons, and it’s worth quickly summarizing in which situation you would chose one instead of another.

asbundle

Its whole source code is ridiculously small, about 100 lines of code focused on doing only one thing: put ascjs generated output together in a tiny and super fast cross browser wrapper.

It leaves the code as it is, it weights nothing as npm dependency, and it’s based on the fast, battle tested, and relatively small Esprima library.

If you can survive without ESM live bindings and you are publishing a library, application, or module, that doesn’t need code splitting, this project might be all you need to easily bundle for any browser.

browserify

It’s one of the most popular and fast bundlers and, differently from others, it has the ability to bring various NodeJS core modules to the browsers.

Unfortunately, browserify is not capable (yet) of parsing modern browser-friendly ECMAScript code, so that you need to write your source code as CommonJS even if you’re targeting browsers that would just work as ESM.

rollup

I’m impressed by its potentials and I need to dig more about that little global leak gotcha. This project seems to pack best features such plugins and “tree shaking”, and it always produced in my tests the smallest bundle.

If you don’t care about bundler size and you couldn’t care less about dealing with a final bundle that doesn’t even look close to what you initially wrote, also considering source maps are available, I’d give this bundler a chance.

webpack

Battle tested, well documented, easy to use, and with a great community and advocates behind, if only it wouldn’t weight 16MB+ per each project that uses it, and I’m not considering extra possible plugins at all, I can say for experience I’ve always found great features here and it served me well.

Automatic asynchronous code splitting on dynamic import(...) is also an excellent feature you have out of the box, and I think there’s a plugin for everything already. A must try for bigger projects and/or modern PWAs.

--

--

Andrea Giammarchi
Andrea Giammarchi

Written by Andrea Giammarchi

Web, Mobile, IoT, and all JS things since 00's. Formerly JS engineer at @nokia, @facebook, @twitter.

No responses yet