Getting Real Dead-Code Elimination in ClojureScript with Shadow‑CLJS Advanced Builds
Learn how to enable genuine dead‑code elimination in ClojureScript using Shadow‑CLJS with :optimizations :advanced, verify the bundle size, and avoid common interop pitfalls.
12 Dec 2025, 21:22 UTC

Why advanced compilation gives you true dead‑code elimination
The headline engineering feature of ClojureScript is whole‑program dead‑code elimination. When you build with :optimizations :advanced the Google Closure Compiler analyzes the entire program, removes every function and namespace that is never reached, and renames the remaining symbols. The result is a bundle that contains only the code you actually call – no extra library code unless it is referenced.
Minimal Shadow‑CLJS configuration
Create a shadow-cljs.edn file at the project root with the following minimal browser build:
;; shadow-cljs.edn
{:builds
{:app
{:target :browser
:output-dir "public/js"
:modules
{:main {:entries [my.app]}}}}}
This tells Shadow‑CLJS to compile the namespace my.app (replace with your entry point) and output the bundle to public/js/main.js.
Producing the release bundle
- Ensure you have Node.js installed and Shadow‑CLJS added as a dev dependency (
npm i -D shadow-cljs). - From the project root, run the release command:
npx shadow-cljs release app
This command triggers the :advanced optimization level. No special permissions are required beyond normal user access to the project directory.
Verifying the result
- Check the size of the generated bundle (
public/js/main.js). It should be noticeably smaller than the dev build (npx shadow-cljs watch app), often an order of magnitude reduction. - Generate a build report to see what contributed bytes:
npx shadow-cljs run shadow.cljs.build-report app
The report lists each namespace and its byte contribution. Namespaces you never require should show 0 B. If you see unexpected size from libraries like cljs.spec or cljs.pprint, trace back to a stray :require and remove it.
Common pitfalls and how to avoid them
Property renaming breaks plain‑JS interop
Under :advanced the Compiler renames object properties to shorten names. Accessing a renamed property via aget or dot notation will return undefined in the release build while working in dev.
Supported fixes:
- Use
goog.object/get(or the helper(gobj/get obj "key")) to read foreign data with string keys. - Add
^jstype hints and use.-fieldaccess so the Compiler knows not to rename that property. - Provide an externs file that declares the property names you need to keep.
Reflection‑style vars disappear
Code that resolves vars by string name (e.g., (resolve "my.ns/fn") or intern) relies on the Compiler’s symbol table. Advanced compilation may rename or drop those vars, causing runtime nil or errors.
Avoid dynamic var lookup in production code; if you need it, keep the relevant namespaces in :advanced by adding a dummy reference or using :foreign-libs with appropriate externs.
Externs must stay in sync with JS libraries
If you depend on a JavaScript library and write externs for its API, upgrading the library without updating the externs can reintroduce the “undefined at runtime” bug.
Practical check: after any JS dependency upgrade, run the release build and exercise every interop path (API responses, third‑party widgets). If you see missing data, revisit the externs.
Limitations
Advanced compilation is most effective when your code avoids:
- Eval‑like constructs (
eval,requirewith strings) that hide dependencies from the Compiler. - Heavy use of macros that generate code based on runtime strings.
- Reliance on the exact names of generated functions or properties (except when protected via
^jsor externs).
These patterns can cause the Compiler to retain more code than expected or to break at runtime. Keep them isolated to development‑only namespaces or provide proper externs/type hints.
By following the configuration above, verifying with a build report, and testing the release artifact for interop, you can ship ClojureScript applications that truly contain only the code you use.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.