ClojureScript :advanced Optimization: Dead-Code Elimination, Interop, and Bundle Size
Why :advanced shrinks ClojureScript bundles, how dead-code elimination works, and what you must handle for interop and debugging.
14 Feb 2026, 04:46 UTC

Problem: Default ClojureScript bundles are verbose
When you build a ClojureScript project with the default :simple optimization, the compiler keeps all symbol names and often includes code that never runs at runtime. This makes debugging easier but results in larger JavaScript files, especially for apps with many namespaces.
Thesis: :advanced optimization can dramatically shrink your output
By enabling :advanced, the Google Closure Compiler performs aggressive dead-code elimination and symbol mangling. Functions, vars, and macros that are not reachable from your entry point are stripped, and remaining identifiers are renamed to shorter forms. The trade-off is that you must manage interop and debugging carefully.
1. How :advanced optimization works
The compiler runs the Google Closure Compiler in advanced mode. Two things happen:
- Dead-code elimination: Any namespace, function, or var not reachable (directly or transitively) from the root namespace or
cljs/closure-entry-pointis removed from the output. - Symbol renaming (mangling): Remaining identifiers are replaced with short, cryptic names like
a,b, etc., reducing byte count.
Unreachable code includes unused helpers, test-only functions, and macros expanded only in development.
2. The interop challenge and practical solutions
Renamed symbols can break interop with hand-written JavaScript or libraries that rely on specific global names. ClojureScript provides two mechanisms to preserve names:
^:exportmetadata on a var: tells the compiler to keep the original name in the output.externsfiles: declare the shape of external JavaScript so the compiler does not rename or remove it.
For example, if you wrap a jQuery plugin, adding ^:export to the entry point var ensures the Closure Compiler keeps that name intact.
3. Debugging with source maps and the production trade-off
Source maps map the compiled JavaScript back to original ClojureScript, enabling stack traces and REPL-driven debugging. Enable them with :source-map true in your build config. Note that source maps increase build time and, if not minified, can offset the size gains from advanced optimization.
In production, you typically disable source maps or serve the minified version without them.
Worked example: Enabling and verifying :advanced optimization
Add the following to your project.clj or deps.edn build profile:
(defproject my-app "0.1.0"
:cljsbuild {:builds {:app {:optimizations :advanced
:source-map true
:output-to "resources/public/js/app.js"
:figwheel {:on-reload "fn-name"}}}}
:profiles {:advanced {:optimizations :advanced}})
Alternatively, with deps.edn:
{:paths ["src" "test"]
:aliases {"advanced" {:extra-paths ["resources/cljs"]
:extra-deps {nrepl/clojure-mode {:mvn/version "0.5.0"}}
:exec-fn cljsbuild.exec/cljsbuild-main}}}
Run the optimization from your project root:
Command: cljsbuild once advanced (or lein with-profiles +advanced cljsbuild once)
Where to run: Project root directory. No special permissions required; the command reads source files and writes to the output directory.
Meaningful placeholders: :optimizations :advanced, :source-map true, :output-to "resources/public/js/app.js".
Expected checks: After the build, open the generated app.js. Functions you defined but never called from your entry point should be absent. Symbol names that were originally long (e.g., my-app.util.helper) will appear as mangled short names.
Relevant risks: If a library you depend on uses goog.provide/goog.require patterns that the Closure Compiler does not understand, some code may be incorrectly stripped. Always run a smoke-test of your app after the first :advanced build.
Limitation: Source-map size and build time
Enabling :source-map true adds a separate .map file and increases the overall bundle size. For CI/CD pipelines where only the minified JS is deployed, disable source maps or use :source-map {:inline false} to keep the map external but minify the JS separately.
You can verify the impact by comparing the file sizes of a :simple build versus a :advanced build with and without source maps.
Actionable closing: When to reach for :advanced
- Your app has many namespaces and you want the smallest possible JS payload.
- You can annotate interop points with
^:exportor provide externs. - Your CI pipeline can tolerate a slightly longer build time for the size savings.
Start with a single build profile, verify dead-code removal, and only expand :advanced to the full app once you are comfortable with the interop adjustments.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.