Shave 20% off your optimized ClojureScript
ClojureScript uses the Google Closure compiler, which not only mangles and minifies code, it also inlines functions and removes dead code. Still, every extra KB in your application is distributed to every client who wants to use it. So how can we make it even smaller?
When looking to shrink the client-side source for safepaste, a security-conscious paste site, I looked a bit outside the ClojureScript box and found a Node.js plugin called uglify-js. Among a myriad of features, uglify-js also contains its own mangler, optimizer, and dead code remover. Much to my surprise, this optimizer works wonders, even on Closure-optimized ClojureScript code.
Assuming you have Node.js installed, uglify-js can be installed in the current directory with npm.
$ npm install uglify-js
Once installed, uglify-js can be used directly on some JS source, whether it’s compiled ClojureScript or a third party JS library (or both).
$ ./node_modules/uglify-js/uglifyjs --screw-ie8 -c -m -- main.js
It contains a slew of parameters, many with a number of their own options, but the above will get you as far as 20%, in my tests, on already-optimized ClojureScript code.
Two primary use cases for bringing in uglify-js are apparent to me:
- Minifying third party JS libraries which preface your optimized CLJS
- Further minifying your optimized CLJS to shave off even more fat
The good news: you can have them both!
Integrating into boot
For those who’ve migrated to boot, a build
framework for Clojure, here’s a
task which will minify your
main.js into a
main.min.js. It handles installing uglify-js,
minifying, and it falls back to not copying if npm isn’t installed.
(deftask minify "Minify the compiled JS"  (fn [next-task] (fn [fileset] (let [tmp (tmp-dir!) old-file (tmp-get fileset "js/main.js") old-file-path (-> old-file tmp-file .getPath) new-file (io/file tmp "js/main.min.js") new-file-path (.getPath new-file) node-modules "./node_modules/uglify-js"] (try (io/make-parents new-file) (println) (when (not (fs/exists? node-modules)) (println "Installing uglify-js...") (shell/sh "npm" "install" "uglify-js")) (println "Minifying JS...") (shell/sh (str node-modules "/bin/uglifyjs") old-file-path "--screw-ie8" "-c" "-m" "-o" new-file-path) (let [original-size (fs/size old-file-path) new-size (fs/size new-file-path)] (println (format "Shaved off %.2f%%\n" (float (* 100 (- 1 (/ new-size original-size))))))) (catch Exception _ (println "npm isn't working; not minifying...") (fs/copy old-file-path new-file-path))) (next-task (-> fileset (add-resource tmp) (rm [old-file]) commit!))))))
Add the task definition to your
build.boot, call it in your
tasks after the
cljs task, and reap the benefits (example). Be sure to check that your
HTML is including the minified version of the JS! The task will boast its
success upon running.
2016-02-16 17:52:03.094:INFO::main: Logging initialized @4313ms Compiling ClojureScript... • js/main.js Minifying JS... Shaved off 19.35% Compiling 1/1 safepaste.core... 2016-02-16 17:52:32.400:INFO::clojure-agent-send-off-pool-0: Logging initialized @33619ms Writing pom.xml and pom.properties... Adding uberjar entries... Writing safepaste.jar...
In combination with gzipping
In hopes of shaving off even more fat, you can use some ring middleware, like ring-gzip, to serve your content zipped. Fortunately, the gains of uglify-js are still visible in the gzipped version, sitting at nearly 10% smaller than the gzipped original source.
To boot users: you’re out of luck. With leiningen, however, there is lein-asset-minifier. For those interested, I’ve found that boot-cljsjs is using lein-asset-minifier within a boot POD. The source can be found here.
Based on my research, uglify-js is the front-runner in the minification game. The “impurity” of bringing in npm for the job is entirely worthwhile for me, considering the palpable gains.