© 2021. All rights reserved.

Bundler Showdown

January 1, 2021

With the addition of :target :bundle into clojurescript, I've wondered about how a javascript bundler would compare to my build tool of choice, shadow-cljs.

I put together a script which tries webpack, browserify, esbuild, parcel, and shadow-cljs on one of my medium sized projects, portal. Here is what the package.json looks like for the production dependencies:

packge.json
{
  "dependencies": {
    "papaparse": "^5.3.0",
    "react": "17.0.1",
    "react-dom": "17.0.1",
    "react-visibility-sensor": "^5.1.1",
    "vega": "^5.17.3",
    "vega-embed": "^6.14.2",
    "vega-lite": "^4.17.0"
  }
}

The most recent dependency, vega, more than doubled the bundle size of the project. This really encouraged me to take a look into the bundlers.

Results

Here are the production bundle sizes sorted from smallest to largest:

bundlersizegzipversionsrelative
webpack1.5 MiB454.0 KiBwebpack 5.11.1, webpack-cli 4.3.0
-15.3%
browserify1.6 MiB503.2 KiB16.5.2
-6.1%
esbuild1.7 MiB527.0 KiB0.8.28
-1.7%
shadow-cljs1.8 MiB535.9 KiB2.11.10
0%
parcel2.1 MiB579.3 KiB1.12.4
8.1%

I don't think these results are too much of a surprise as webpack is the most popular javascript bundler. Packages tend to optimize for it and it has seen an order of magnitude more contributions than any other bundler. At the time of this post, here are the commit stats:

bundlercommits
webpack12631
browserify2287
esbuild1575
parcel2125

After loading up all output bundles in the browser and running my typical end-to-end tests, no issues were found. The bundles seem fully functional. I think as long as you can get past the :advanced compilation, things should be okay.

Issues

I did run into CLJS-3258 with reagent which bring in some redundant cljsjs react artifacts from clojars which are already being pulled in via npm. This was solved with the following exclusion:

deps.edn
reagent/reagent {:mvn/version "1.0.0"
                 ;; Needed to work with :target :bundle when building
                 ;; with clojurescript. These deps are pulled in via
                 ;; npm via the package.json already.
                 :exclusions [cljsjs/react-dom
                              cljsjs/react
                              cljsjs/react-dom-server]}

Script

To run the experiment yourself, you can try the bundlers portal branch.

git clone https://github.com/djblue/portal.git
cd portal
git checkout bundlers
make bench/bundlers

Conclusion

My results are not going to be directly applicable to other projects so I would encourage you to experiment before making any decisions. At this time it does look like switching to webpack will save me about 15% on my bundle size. I don't think I'll stop using shadow-cljs anytime soon as it's much more than a bundler, but I might start using webpack for production builds.

Happy Bundling!

Edit: Something that I wasn't aware of but just learned about is the :js-provider :external (:target :bundle for shadow-cljs) which is designed to support this exact use case!