Choosing the Right JavaScript Bundler in 2020

Why we’ve chosen Parcel for our microfrontend framework Piral

Florian Rappl
Bits and Pieces

--

Image by Arek Socha from Pixabay

Some time ago resource bundlers went from an exotic technology to mainstream to a necessary tool when developing modern web applications.

At the heart of modern web apps, a bundler selected what packages to include and what code to throw out. A bundler also decided what resources to consider and how to name them.

The most prominent bundler is without a doubt Webpack. As a project, it started already in 2011, but it took at least until 2014 when it really hit off.

Quite often I get asked why we’ve chosen Parcel for our microfrontend framework Piral. In this article, I’ll shed some light on where Parcel may be a good choice and where others can shine.

A small tip before we start — It’s extremely useful to share UI components between microfrontends, to keep a consistent UI and scale your development.

Use tools like Bit.dev (Github) to share and sync UI components between your microfrontends, while keeping each team independent.

Through Bit.dev each team can harvest and organize their components in a shared collection, let others use and even update components from their own projects, and keep sync throughout the organization and across projects.

Give it a try or learn more here.

Example: shared React components in Bit.dev

Choosing The Right Tool

A bundler is concerned with walking the tree of (JS) modules and producing a distributable output. It does that in three phases:

  1. Collection
  2. Transformation
  3. Optimization

Depending on the bundler the three phases are more or less obvious. While collecting the assets requires knowledge of the underlying source code files (e.g., how are modules referenced from JavaScript, how are assets referenced in HTML, …), the transformation phase is required to normalize the given information.

Working scheme of a web bundler

As a result, various optimizations including tree shaking can be performed. Finally, the gathered bundles are written to the file system.

In general, there are two extremes:

  1. Bundlers that try to do as much as possible within the core (e.g., Parcel, FuseBox)
  2. Bundlers that rely on plugins to fill the different phases with live (e.g., Webpack, Rollup)

Within the two extremes, there are, of course, more variations. As an example, while FuseBox already comes with all plugins available from the core their dependencies must still be installed when required. As such it tries to find a good way between installing a lot of (potentially unnecessary) packages out of box and providing coherent user experience.

Some bundlers are mostly specialized in dealing with JavaScript (e.g., Browserify, Rollup) and will not (fully) work with general web frontend projects. Here, dealing with CSS or its variants, images, and other formats is required.

Still, bundlers may be used for (pure) Node.js projects, too.

Distributing a single JS that already includes all dependencies can be quite beneficial.

Not only will the cold start be much faster as the file system does not be consulted so frequently, the dependency management also becomes much more lightweight.

Right now a lot of different bundlers exist. Arguably, the most popular ones are:

Please let us know if we missed one (e.g., microbundle). We’ll update the GitHub repo.

Let’s just make an evaluation based on a little example project.

A Little Example Project

Consider the following situation: We have a set of files that we want to have bundled and optimized for the web.

We have:

  • one HTML file, arguably the entry point of our web app
  • one SASS file, which should be converted to CSS
  • two TypeScript React files, which should be treated as two bundles (one references the other for a bundle splitting scenario)
  • an image (JPG) that is referenced from one of the TypeScript files

Ideally, we expect to obtain 1 HTML file, 1 CSS file, 2 JS files, and 1 JPG file.

With this project in mind we can create a little “bundler shootout”, i.e., an evaluation over the different bundlers.

The key metrics for this evaluation are:

  • Number of packages to install
  • Number of packages installed
  • Complexity of the configuration, if any
  • Time to bundle — on the first run and subsequent runs
  • Resulting bundle sizes
  • Number of required changes to the original project files

Obviously, since we already start with an existing project some “standards” are already followed. We use

  • import() for async imports (should be used to provide bundle splitting)
  • require() for referencing general resources
  • import from for general JS imports

In the index.html we reference the original files, e.g., app.tsx instead of the name used later (something like app.js).

Evaluation

For the evaluation we used just standard tools. As an example, the number of actually installed packages was just derived from actually reading what npm install had given us back.

Running the actual bundling was always performed via time npm start, where the start script was pointing to the bundler's CLI tooling or a Node.js script using the bundler's API. We always performed three runs. The first measurement was then counted for the initial run, the second one for the subsequent runs, and the last one as a rough confirmation of the second measurement.

The bundle sizes have been identified via a simple ls -l dist statement.

Likewise, the number of changes have simply been counted with a diff. Here, an addition and a deletion (i.e., a modification) was just counted as one. Same goes for moving a file.

The complexity of the configuration was measured in lines of code in the configuration file. The configuration files have been all developed with maintainability in mind. So the general mindshare should be the same.

While I’ve been certainly guilty as charged to more knowledge in some bundlers than others, every bundler was treated the same: We used the GitHub issues, the public documentation, its source code, as well as standard search engine results to derive the configuration.

This is also not the first time bundlers have been evaluated. Actually, making such comparisons is quite common and I would encourage you to do your own comparison when starting a new project.

For the time being just some other comparisons:

In the current post I will only go into the app direction, as for libs the requirement is not that strong (yes, producing an UMD is nice, but you don’t need it as fast as you need a bundler for a web app).

Results

The results are a bit as expected. While Webpack was easy to install at first, we required a substantial amount of boilerplate and plugins to solve this straight forward task. Nevertheless, Webpack’s documentation and ecosystem easily provide everything that we need.

Quite a bit simpler was Parcel. Here, everything just worked out of the box. No additional plugins, configuration or anything else has been required. It just did what it should.

The most extreme contrast was potentially Rollup. At first, we went for a plugin that sounds like it would seal the deal — using an index.html as entry point. However, it turned out that the plugin was no longer maintained and compatible. There was no compatibility check or anything - just a cryptic error that reminded us that something has to be wrong with the configuration.

Rollup also was the most extreme regarding requiring plugins for things that really should work out of the box. An example is the use of CommonJS. Most NPM packages are still (and will be in 2020) shipped as CommonJS modules. Rollup requires a plugin to work with that. Even worse, we need to tell this plugin what it should actually do. Hence we have to duplicate the imports from our code base to the Rollup configuration.

In the end Rollup’s architecture also was not really robust regarding transformations. Apparently, all plugins need to implement all output targets. This cannot possibly scale.

As a result one plugin that we used was not compatible with the desired output. So we ended up with a format that we did not want.

Comparing this to FuseBox, which starts with a different philosophy altogether. Here the optimizations are all performed by the “quantum plugin”. Not sure how a godlike plugin fits in — but it sure seems to work great.

The configuration of FuseBox also represents the its CLI.

With FuseBox we thus get a “construct your own bundler” kind of technology. The ecosystem is — for numerous reasons — not as mature or vast as the one from Webpack, but at least in our test everything seemed to have worked together well.

The older candidates Browserify and Brunch may still be viable for one or the other use case, however, their age has shown in the test. Without any bundle splitting or possibility to reference static assets they may still be viable for use in Node.js projects.

Results of the bundler comparison

The full sample project is available on GitHub. In the provided readme all results are aggregated in a single table.

Conclusion

At least for the evaluated example project the choice is clear. Not always is the situation as described so a closer look at the available bundlers makes definitely sense.

For me personally, the comparison confirmed my preference for Parcel.

While Parcel had a breakthrough last year it certainly grew mature this year. With Parcel 2 around the corner we’ll continue to have an elegant Webpack alternative.

The other options such as FuseBox also bring interesting concepts to the table. In the end it really matters where we want to use these tools and what problem we try to solve.

Learn More

--

--

Technology enthusiast and solution architect. Currently busy with microfrontends.