Choosing a ClojureScript Optimization Level for Production Builds
A concise guide to picking :none, :simple or :advanced for ClojureScript production builds.
17 Jul 2026, 22:04 UTC

Decision and constraints
When you prepare a ClojureScript project for production you must pick a Google Closure Compiler optimization level that balances three constraints: bundle size, runtime performance, and the effort required to keep JavaScript interop stable. The wrong choice can inflate download size, cause subtle runtime errors from missing externs, or lengthen feedback loops during development.
Takeaway: Use :advanced for the smallest, fastest bundle when you can provide correct externs; fall back to :simple if your project relies heavily on reflection‑based JS libraries or you cannot maintain externs.
Comparison of supported options
| Optimization level | What the compiler does | Build size impact | Runtime performance | Setup complexity | JS interop safety | Typical use case |
|---|---|---|---|---|---|---|
:none |
No minification, source names preserved | Largest (baseline) | Baseline | None – just add :optimizations :none |
Full safety – no mangling, no externs needed | Debugging, REPL‑driven development, local demos |
:simple |
Whitespace removal and symbol mangling | Moderate reduction (≈30‑50 % gzipped) | Slightly better than :none |
Low – same as :none |
High – mangling is safe for most JS accessed via js/ |
Stable dev builds, CI previews, when you want smaller payloads without externs |
:advanced |
Aggressive minification, dead‑code elimination, function inlining | Smallest (often 60‑80 % reduction gzipped) | Best – tighter loops, fewer allocations | High – requires externs file and careful review of JS interop | Conditional – safe only when externs cover all reflected JS accesses | Production bundles for browsers or Node.js where size and speed matter |
Trade‑offs explained
- Build size vs. interop risk:
:advancedremoves unused code and shortens names, which can break libraries that rely on reflection (e.g.,obj[prop]wherepropis computed at runtime) unless you declare those names in an externs file. - Setup complexity: Adding an externs file is a one‑time cost, but you must keep it in sync with library versions. If you cannot maintain externs,
:simplegives most of the size benefit with virtually no risk. - Build time: The Closure Compiler’s analysis for
:advancedcan increase incremental build times by 2‑5× compared to:noneor:simple. - Debugging experience: Source‑map quality is similar across levels, but
:noneretains original variable names, making stack traces easier to read without a map.
Concrete implementation (Shadow CLJS)
Place the optimization choice inside the build configuration of shadow-cljs.edn. The example below shows a production build that uses :advanced with an externs file for a popular UI library.
;; shadow-cljs.edn
{:source-paths
["src"]
:dependencies
[[reagent "1.2.0"]
[react "18.2.0"]]
:builds
{:app
{:target :browser
:output-dir "public/js"
:asset-path "/js"
:modules {:main {:init-fn my.app.core/init}}
;; Choose optimization level here
:optimizations :advanced
;; Externs file tells Closure Compiler not to mangle React globals
:externs ["externs/react-18.externs.js"]
;; Optional: enable source maps for debugging
:devtools {:http-port 8000
:after-load my.app.core/reload}}}
}
}
To switch to :simple or :none:optimizations. No other modifications are required.
Validation steps
- Compare artifact sizes: After each build, run
ls -lh public/js/main.jsand record the size. Verify the order:none>:simple>:advanced(or equal if the project has little dead code). - Check for extern warnings: Look in the Shadow CLJS console output for lines like
WARNING: No externs file provided for .... Their presence indicates missing externs for:advanced. - Run headless tests: Execute your test suite against the compiled bundle, e.g.,
npx shadow-cljs compile app && npx karma start --single-run. Ensure no new runtime exceptions appear. - Measure gzipped payload: Use
gzip -c public/js/main.js | wc -cto confirm the bundle stays within your performance budget (commonly < 100 KB gzipped for a typical SPA). - Spot‑check interop: Manually exercise any JS library that uses reflection (e.g., jQuery plugins, Chart.js) and verify that properties are still accessible.
Limitations and practical checks
:advancedmay still break if a library usesevalor dynamically generated property names that are not captured in externs. In such cases, either add the needed names to the externs file or revert to:simplefor that build.- Build times increase noticeably with
:advancedon large codebases; consider caching the Closure Compiler jars or using incremental builds (:browser-testtarget) during development. - Verify the result by checking the generated source map (
public/js/main.js.map) – it should map minified names back to original sources, confirming the compiler ran successfully.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.