Elm minification with ESBuild?

It seems like esbuild can perform minification significantly faster than terser.

I’m not sure how to get the equivalent steps with esbuild of the recommended Elm minification commands, which currently use terser with some options for marking pure functions.

There’s a GitHub issue that discusses this, but I didn’t fully grasp the conclusion of the thread or whether it resulted in minified output that was as good as the standard terser commands.

Has anybody had success with this? It seems like it could be quite nice to use that to speed up our build pipelines!

3 Likes

I use these 2 commands at the moment:

elm-optimize-level-2 --output build/client.js source/Client.elm

esbuild --bundle --minify --pure:A2 --pure:A3 --pure:A4 --pure:A5 --pure:A6 --pure:A7 --pure:A8 --pure:A9 --pure:F2 --pure:F3 --pure:F3 --pure:F4 --pure:F5 --pure:F6 --pure:F7 --pure:F8 --pure:F9 --outfile=build/initializeClient.js script/InitializeClient.mjs

I’ll post the benchmarks here once I get around to it. I’ll benchmark against Terser and Google Closure-Compiler, as they did the best minification-job last I checked

1 Like

You may also be interested in GitHub - privatenumber/minification-benchmarks: 🏃‍♂️🏃‍♀️🏃 JS minification benchmarks: babel-minify, esbuild, terser, uglify-js, swc, google closure compiler. I know the author would be happy to have more contributions if you have them.

2 Likes

Observations

Measured based on build-artifacts from rtfeldman/elm-spa-example using Elm 0.19.1

Minifier Version Output Gzipped
none N/A 373.645kb 66.471kb
esbuild(without pure-flags) 0.9.6 117.121kb 36.619kb
esbuild(with IIFE-flag) 0.9.6 117.121kb 36.616kb
esbuild 0.9.6 117.121kb 36.611kb
google-closure-compiler (with advanced-flag) 20210302.0.0 Error N/A
google-closure-compiler 20210302.0.0 97.398kb 31.157kb
terser 5.6.1 94.652kb 29.824kb
uglify-js 3.13.3 92.621kb 29.508kb

Evan Wallace did comment on the known limitations of the optimization-pass:

It could still make sense to build an advanced optimization pass into esbuild at some point in the future, but I’d like to direct development efforts on getting more of esbuild’s end-to-end bundling story in place first. Code splitting, top-level await, CSS, and also maybe HTML are all more of a priority for me, especially since Terser is moving to native code.

Method

Gzipped using gzip -k build/*
Measured using wc -c build/*

# build Elm code
elm make --optimize src/Main.elm --output=build/elm.js

# Optimize with EsBuild. Add '--format=iife' or remove all '--pure:' flags to run it in the other modes.
esbuild --bundle --minify --pure:A2 --pure:A3 --pure:A4 --pure:A5 --pure:A6 --pure:A7 --pure:A8 --pure:A9 --pure:F2 --pure:F3 --pure:F3 --pure:F4 --pure:F5 --pure:F6 --pure:F7 --pure:F8 --pure:F9 --outfile=build/elm.esbuild.js build/elm.js

# Optimize with Uglify-js
uglifyjs build/elm.js --compress 'pure_funcs=[F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9],pure_getters,keep_fargs=false,unsafe_comps,unsafe' | uglifyjs --mangle --output build/elm.uglify.js

# Optimize with Terser. Config is pasted from Uglify-js
terser build/elm.js --compress 'pure_funcs=[F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9],pure_getters,keep_fargs=false,unsafe_comps,unsafe' | terser --mangle --output build/elm.terser.js

# Run Google-closure-compiler in simple mode, as advanced fails for some reason...
google-closure-compiler --js build/elm.js --js_output_file build/elm.google-closure-compiler.js
8 Likes

I ran @opvasger’s commands at work.

Minifier Version Time Output Gzipped
none N/A N/A 3.0 MiB 447 KiB
esbuild 0.9.6 0.2 s 766 KiB 236 KiB
google-closure-compiler (with advanced-flag) 20210302.0.0 13 s 708 KiB 220 KiB
google-closure-compiler 20210302.0.0 8 s 712 KiB 219 KiB
terser 5.6.1 10 s 703 KiB 215 KiB
terser single command 5.6.1 8 s 702 KiB 215 KiB
uglify-js 3.13.2 15 s 694 KiB 214 KiB
uglify-js single command 3.13.2 14 s 689 KiB 215 KiB

Note: My results use 1 KiB = 1024 bytes, while @opvasger seems to use 1 kb = 1000 bytes. I used ls -lh build to measure.

Some notes about esbuild (all sizes below are non-gzipped):

  • Not using --bundle gives slightly smaller results. 0.2 KiB
  • You need to use --target=es5 if you want to keep ES5 output. Otherwise esbuild will “upgrade” your syntax and use => functions, for example. --target=es5 increases the size somewhat. 0.2 KiB
  • The --pure:X flags seem to make no difference whatsoever in my testing. Maybe we use all of those functions at work.
  • Manually removing the IIFE from Elm’s JS output results in smaller esbuild output. Use --format=iife to add the IIFE back. 15.3 KiB

Using esbuild is so tempting since it’s like 40 times faster (finishes in less than a second)!

6 Likes

Too bad the results aren’t as compact with esbuild! But that said, I guess it is still a work-in-progress, so it’s possible that in the future the minification will improve.

This video touches on the current limitations with esbuild’s minification, and talks about how esbuild is the right fit if your priority is speed, but if bundle size is important than you’re better of using terser:

Then the way that esbuild’s minification works is naive. And I use the word “Naive,” but naive implies that it’s worse than it is. It shortens variable names. It removes whitespace and it does – it does like old-school style minification before as it did with terser and stuff. Which means your minified builds would be bigger than if you ran it through a terser. The answer is take the esbuild bundle and run it through Terser. You should probably do that if bundle size is a big priority for you. And even then, it will still be a lot faster.

So I guess for now the takeaway is to keep using terser (unless a fast minification pass is your priority). And we’ll have to stay tuned to see if esbuild can compete with bundle size.

2 Likes

Thanks everyone for chiming in with these very helpful tables of data! This will make this thread a good reference for this topic :+1:

1 Like

tips hat

I see you’re a man of greater accuracy - I couldn’t even get the capitalization right :rofl:

I’ll try and remember to use kibibytes from now on.

1 Like

How about combining two or more minifiers, one after the other? :exploding_head:

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.