Reducing ClojureScript Bundle Size with Advanced Compilation and Externs Inference
Learn how to shrink ClojureScript output by turning on advanced compilation and letting the compiler infer externs, with a step‑by‑step Reagent example and verification tips.
25 Feb 2026, 21:38 UTC

Problem: naïve builds produce large JavaScript
When you start a ClojureScript project with the default :optimizations :none setting, the compiler emits a straightforward translation of your source files. The resulting JavaScript is easy to debug but contains every symbol, including unused library code, and retains original names. On mobile networks this can add hundreds of kilobytes to the initial download, slowing page load.
Thesis: enable :optimizations :advanced and let the Closure Compiler infer externs
The Closure Compiler’s advanced mode performs aggressive dead‑code elimination and renames symbols to short, unique identifiers. If the compiler knows which JavaScript names must stay unchanged (externs), it can safely mangle everything else. ClojureScript can generate those externs automatically, so you get smaller bundles without manually maintaining an externs file.
How advanced compilation works
In :advanced mode the compiler builds a call graph from your entry points. Any function or property that cannot be reached is discarded. Remaining symbols are renamed during the mangle pass. For JavaScript interop the compiler treats any name that appears in an externs file as “external” and leaves it untouched. When you enable :infer-externs true (the default for :advanced in recent ClojureScript releases), the compiler scans your code for property accesses like (.-foo obj) or (.. obj -bar -baz) and adds the corresponding names to the generated externs.
Worked example: moving a Reagent app from :none to :advanced
Start with a minimal Reagent project (using
deps.ednorlein). Ensure you have a build alias that currently uses:optimizations :none:;; deps.edn {:deps {org.clojure/clojurescript {:mvn/version "1.11.132"} reagent/reagent {:mvn/version "1.1.0"}} :aliases {:build {:exec-fn cljs.main/main :exec-args {-c "myapp.core" -O none -r}}}Run the development build to confirm the app works:
clojure -M:buildOpen the page in a browser and verify that the UI renders.
Create a release alias that switches to advanced mode and requests externs inference:
;; deps.edn (add or edit) :aliases {:release {:exec-fn cljs.main/main :exec-args {-c "myapp.core" -O advanced -d "target/release"}}}Compile the release:
clojure -M:releaseThis produces
target/release/main.jsand, alongside it, an externs filetarget/release/externs.js.Check the size difference (you will see a smaller file after the advanced build):
ls -lh target/release/main.js # compare with the none‑optimized output if you kept it ls -lh target/none/main.jsInspect the generated externs to confirm which symbols were preserved:
cat target/release/externs.js // Example output (not exhaustive): var React; var reagent.core; var reagent.impl.component; // …Run your test suite (or manually exercise the app) against the advanced build. If any interop fails, the browser console will show errors like “property ‘foo’ is not defined” indicating a missing extern.
# Assuming you have a test runner that can load the compiled JS clojure -M:test -O advanced
Trade‑off and limitation
Advanced mode removes code that the compiler cannot prove is reachable. If your application accesses JavaScript properties through strings ((goog.object/get obj "dynamicProp")) or relies on reflection, those names may be mangled and cause runtime errors. Because externs inference is heuristic, libraries that use dynamic property lookup or heavily rely on with-style patterns may need explicit externs entries or a :foreign-libs declaration to preserve the required symbols.
Practical way to check the result: after each change to your externs (or after adding a new library), rebuild with :advanced and run the full test suite. If tests pass, you can be confident the inferred externs are sufficient for that version of the library.
Actionable closing
Begin with a working :none build, switch to :advanced with externs inference enabled, and iterate:
- Compile the release build.
- Verify bundle size with
ls -lh. - Run tests or manual checks.
- Examine
externs.jsfor missing symbols and add them manually if needed. - Repeat until the test suite passes and the size reduction meets your goal.
By following this loop you obtain a production‑ready ClojureScript bundle that is significantly smaller while preserving the JavaScript interop your application depends on.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.