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:
{
"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:
bundler | size | gzip | versions | relative |
---|---|---|---|---|
webpack | 1.5 MiB | 454.0 KiB | webpack 5.11.1, webpack-cli 4.3.0 | -15.3% |
browserify | 1.6 MiB | 503.2 KiB | 16.5.2 | -6.1% |
esbuild | 1.7 MiB | 527.0 KiB | 0.8.28 | -1.7% |
shadow-cljs | 1.8 MiB | 535.9 KiB | 2.11.10 | 0% |
parcel | 2.1 MiB | 579.3 KiB | 1.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:
bundler | commits |
---|---|
webpack | 12631 |
browserify | 2287 |
esbuild | 1575 |
parcel | 2125 |
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:
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!