I remember with 0.15 was released, it was stated that esbuild was often a smaller bundle than purs bundle
. But in all the cases I’ve tried, this is not true at all. esbuild
either does no DCE or much much worse DCE than purs bundle
and zephyr
is required to get them to be roughly the same size. I’m wondering if there’s an option I’m missing somewhere.
What’s the command you are using to run esbuild
?
echo 'import { main } from "./output/Main/index.js"; main()' | esbuild --bundle --format=esm --outfile=main.js
That looks right. How do the bundle sizes compare?
And are you not using --minify
intentionally?
Correct, cuz I was comparing esbuild output with purs bundle
output, which doesn’t have a minify flag.
no zephyr
- purs bundle: 292 KB
- esbuild: 1.3 MB
- esbuild --minify: 581 KB
zephyr
- purs bundle: 296 KB (now sure how this is bigger, but this tracks somewhat with my genearl experience of zephyr not doing much for my bundle sizes)
- esbuild: 296 KB
- esbuild --minify: 121 KB
If you’re using purs bundle
, then you must still be on PureScript 0.14.x
, correct?
Yes, for anything that I have done a direct comparison of, it’s been 0.14 projects.
Ah, that explains why then. See Tree Shaking. The relevant parts from those docs are quoted below:
Tree shaking is the term the JavaScript community uses for dead code elimination, a common compiler optimization that automatically removes unreachable code. Within esbuild, this term specifically refers to declaration-level dead code removal.
In a paragraph towards the bottom:
Note that esbuild’s tree shaking implementation relies on the use of ECMAScript module
import
andexport
statements. It does not work with CommonJS modules.
And…
By default, tree shaking is only enabled either when bundling is enabled or when the output format is set to
iife
, otherwise tree shaking is disabled.
So, until you use PureScript 0.15.x, you won’t get ES6 modules, so esbuild
won’t do any DCE. Now that we have ES6 modules in PS 0.15.x
, it’s my understanding that DCE-tools like zephyr
are supplanted by tools like esbuild --bundle
.
As @JordanMartinez stated, the claim that esbuild (or other JS bundlers) will produce a bundle smaller than that of purs bundle
is specifically for PureScript 0.15. When using webpack at work with PureScript 0.15 we saw about a 30% reduction in our bundle size at work as compared to Zephyr + webpack with PureScript 0.14. The 0.15 series of the compiler switched to ES modules as output and added purity annotations, which go a long way towards helping JS bundlers eliminate dead code in the generated JS.
Ah, okay. thanks for clearing that up.
I find a 30% reduction quite surprising given that I thought zephry did as much DCE as one could possibly do. Do you by chance know how esbuild saved so much space?
Zephyr works on the compiler’s CoreFn output, so it performs dead code elimination on PureScript code. Then, it defers to the compiler’s code generation to produce JavaScript, which you then would pass through a JavaScript bundler such as webpack, parcel, or esbuild. These tools did not do well with the JavaScript produced by the compiler. The big difference in bundle size is coming from the ability of these tools to work with the new JavaScript output.
As an example, I don’t know whether Zephyr is able to remove unused instances or optimize out unused type class dictionaries. It may assume they’re necessary. But with purity annotations and ES modules in the JavaScript the bundlers can remove them. We ran some tests on bundle sizes in this PR:
At the time, Zephyr didn’t work with PureScript 0.15. Now that it supports PureScript 0.15 it would be interesting to see if there is any difference. Definitely it’s the case that PureScript tooling to do an optimization pass before providing output to JS bundlers can produce better output than just bundling the compiler output directly, but I don’t know how effective Zephyr is at that optimization pass.
The module encoding is purs bundle is really heavyweight. All of the imports and exports are still there (and can be significant). Most bundlers operating on ESM will eliminate that.
Specifically for our codebase (we didn’t use purs bundle), the DCE couldn’t be 100% effective because we import some things from JS. We needed to mark any module that JS imported as “do not eliminate”, though I wouldn’t expect this to account for a very significant amount of unused code, since it was all pretty much used. I suspect that bundlers may also be better at eliminating unused FFI code with ESM.
At the CoreFn level these are all just records and functions. If Zephyr were to not eliminate unused instances, then it would have to make a very concerted effort to do so. They should be dropped like anything else.
I just tried zephyr 0.5 on purescript-language-server and it made no/negligible difference.