Choosing the Right JavaScript Bundler in 2020
Why we’ve chosen Parcel for our microfrontend framework Piral
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.
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:
- Collection
- Transformation
- 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.
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:
- Bundlers that try to do as much as possible within the core (e.g., Parcel, FuseBox)
- 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 resourcesimport 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:
- Garima’s take includes also the dev server / HMR setup
- Manisha introduces three kinds of projects and gives every bundler its space
- Brian also includes task runners such as Gulp or Rollup follow-ups such as microbundle
- There are even apps that try to automatically do some comparison for you
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.
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.