DEV Community

Downsize your JavaScript: Mastering Bundler Optimizations

Filip Sobol on December 17, 2024

Introduction Over the past 15 years, the JavaScript ecosystem has expanded rapidly, introducing countless tools to make development easi...
Collapse
 
yyx990803 profile image
Evan You • Edited

There are some notable issues that need to be pointed out for this comparison:

  1. You are using Parcel's build time from a cached run. On my machine, a fresh build with Parcel is about 20x slower than a cache hit.

  2. Your bundle size comparison is not just comparing bundlers, but a combination of bundlers + minifiers. This is quite difficult to make it apples-to-apples because some bundlers come with built-in minification, some require external minifier via plugins, and most of them allow switching between minifiers. The choice of which minifier to use is full of speed vs. quality trade-offs in itself. For example, Vite uses esbuild as the minifier by default, but can use terser or swc instead. In your benchmark, you are using esbuild as the minifier in the rollup config, but using terser in the webpack config. Terser is significantly slower than esbuild, but yields better minification ratio (see github.com/privatenumber/minificat...) If you want to compare only bundlers, then you should do it without minification, but that will not reflect production cases; if you want to compare bundle + minification, then you should at least use the same minifier for bundlers that don't have built-in minification.

  3. Why Rolldown's bundle size look big in this case: while Rolldown has built-in minification (via Oxc minifier), it is still WIP. It only implements very rudimentary compression and is in there for integration test purposes, and has a LOT of room for improvements in the next few months. We should probably emit a warning when users use Rolldown's built-in minify before it is ready. For now, we recommend using a more mature minifier via a plugin. If you use swc as the minifier via a plugin with Rolldown, you should see similar bundle size compared to Rollup.

  4. esbuild has a unique advantage in this benchmark in that its built-in minification adds very little overhead compared to its non-minified build, because it parallelizes many minify-related operations in its per-module transform phase, and also performs the final minification on the same AST. This architectural choice results in better performance, but limits the amount of cross-module optimizations that can be performed. In Rolldown / Oxc, we have opted to perform minification on a separate AST on the bundled chunks in order to be able to add more cross-module-analysis based optimizations down the road. This sacrifices some performance when the minifier is enabled, but will result in smaller bundles in the long run.

  5. I'm not sure how you are measuring the numbers. Usually, numbers reported by the tools themselves omit some necessary overhead (e.g. launching the CLI, parsing the config etc.), where as end-to-end time via npm scripts includes unnecessary overhead of npm or the script runner. There is also a lot of noise / fluctutation between manual single runs. I think a more accurate way to report numbers is using hyperfine to run the builds using Node 22's node --run so that we measure the end-to-end time without npm run overhead.

Collapse
 
filipsobol profile image
Filip Sobol • Edited

That's good feedback, which I was kind of hoping for. I will address some of the points:

  1. The cold build time in Parcel is indeed much slower. Parcel is a fairly unique bundler compared to others in all aspects, and one of the design decisions they made is that cache is enabled by default. This contrasts with other bundlers, which require explicit configuration or don't have caching at all. That's why I decided that using cached build time is fair in this case. When developers choose to use Parcel, they are likely aware of this trade-off and persist the cache locally or in CI/CD. However, I believe that your argument is still valid, and I will update the article to include cold and warm build times as well.
  2. One clarification — the webpack example uses swc minifier, not terser (see the configuration). My reasoning for using different minifiers is that some bundlers have them built-in while others don't. This means that depending on the bundler, the developers are either expected or not expected to bring their own minifiers. I wanted to show how the bundlers perform in the common scenario so that we don't try to compare unlikely setups. That's why currently esbuild, Parcel, Rolldown and Rspack use their own minifiers, while Rollup and webpack use esbuild and swc respectively. This is indeed not a perfect comparison. I'm happy to update the article if you have suggestions, but again — I would rather not compare unlikely setups.
  3. I will update the article with the results of using esbuild minifier with Rolldown. This will probably affect the build time, but will be a more fair comparison. I've tried to be fair to Rolldown and in two places in the article I've mentioned that it's still in alpha and that it may not be fair to draw the conclusions just yet.
  4. Thanks for the detailed explanation. I don't think it makes the comparison unfair, but it's good to know the reasoning behind the numbers.
  5. This is a fair point. I ran the benchmarks many, many times and there was some fluctuation in the numbers, but nothing that would change the overall picture presented in the article given the differences between the build times. However, to make the comparison more fair, I will update the article with the results of using hyperfine (or similar) to run the builds.
Collapse
 
filipsobol profile image
Filip Sobol

The article is updated to:

  • show mean build time from 10 runs (with 2 warm up runs),
  • specify minifiers used,
  • show separate results for Parcel with cache and without cache,
  • show Rolldown results when using minifier from the rollup-plugin-esbuild plugin.

The updated results put Rolldown in the group of best performing bundlers when it comes to bundle size. This is very impressive, given that it's still in alpha.

Thread Thread
 
yyx990803 profile image
Evan You

Thanks for the update!

I noticed another thing: Rollup and Rolldown are showing unreasonably large bundles for MobX and tippy.js, and found out that their configs don't have the necessary options to replace process.env.NODE_ENV for treeshaking. PR: github.com/filipsobol/bundle-size-...

Thread Thread
 
filipsobol profile image
Filip Sobol

Nice catch! I've also disabled the module preload polyfill in Vite because it was added to every result separately, skewing the results, while in real application will only be added once.

The article is updated.

Collapse
 
ben profile image
Ben Halpern

Thanks for the comment!

Collapse
 
fyodorio profile image
Fyodor

In my humble opinion (and experience) build times don’t matter at all if compared to testing/linting processes timing at CI/CD pipelines. So the bundle size is eventually a much more important metric here — at least for large enterprise codebases.

Collapse
 
skillboosttrainer profile image
SkillBoostTrainer

This is an excellent overview of modern bundlers and their performance! I had no idea the differences in build times and output sizes could be so dramatic. Definitely trying out esbuild for my next project! 🚀

Collapse
 
scooperdev profile image
Stephen Cooper

Thanks for pulling all this information together and sharing your insights!

It's been my world for the last 6 months reducing the bundle size of AG Grid!

Collapse
 
reinmarpl profile image
Piotrek Koszuliński

Greetings from CKEditor! Shipping JavaScript libraries is much fun, isn't it, Filip? :D

Collapse
 
kurealnum profile image
Oscar

Thanks for gathering all of this information. I've been trying to optimize some of my stuff for a while, and this really helps!

Collapse
 
jserfeng profile image
Finn • Edited

Thanks for the great article !

I've noticed that Webpack and Rspack supports context require by default, eg: require('./' + name), but rollup seems doesn't support it by default, so moment is bundled by webpack but not rollup in some cases, that affects bundle size as well

Collapse
 
filipsobol profile image
Filip Sobol

This case is discussed here github.com/filipsobol/bundle-size-...