Unlocking ClojureScript’s Smallest Builds: A Practical Guide to Advanced Closure Optimizations
Advanced Closure Compiler builds shrink ClojureScript bundles but risk breaking external libs. This guide shows how to enable :advanced, declare externs, export symbols, and verify the output for a robust production build.
31 Dec 2025, 04:32 UTC

Problem: The Size‑vs‑Compatibility Trade‑off
When you ship a ClojureScript app, you often want the smallest possible JavaScript bundle. The Google Closure Compiler’s :optimizations :advanced flag can trim kilobytes, but it also aggressively renames symbols and removes seemingly unused code. If your code talks to external libraries or uses dynamic property access, a naive advanced build will break.
Thesis: Advanced builds work – if you give Closure the right hints.
The key to a successful advanced build is twofold:
- Generate or provide accurate externs for every external global name.
- Mark any ClojureScript symbols that must survive renaming with
^:exportorgoog.exportSymbol.
Below is a step‑by‑step recipe that shows how to enable advanced optimizations with shadow-cljs, verify the output, and handle common pitfalls.
1. Enable Advanced Optimizations in Your Build
;; shadow-cljs.edn
{:source-paths ["src" "resources"]
:dependencies [[reagent "1.2.0"]]
:builds
{:app
{:target :browser
:output-dir "public/js"
:asset-path "/js"
:modules {:main {:init-fn my.app/init}}
:devtools {:http-root "public" :http-port 3000}
:compiler-options {:optimizations :advanced
:pretty-print false
:externs ["node_modules/react/umd/react.development.js"]}}}}
Run the build with:
npx shadow-cljs release app
Requirements:
- Node.js 18+ (required by shadow-cljs).
- Write permissions to
public/js. - Placeholders: replace
my.app/initwith your own init function.
After the build, you’ll find app.js in public/js. It should contain heavily mangled identifiers (e.g., m, o).
2. Declare Externs for External Libraries
Advanced mode will rename any global names it sees. If you call a function on the global React object, Closure will rename it unless you tell it otherwise.
;; externs/react.js
// externs for React
/** @type {Object} */
var React;
/** @type {function()} */
React.createElement;
Add the extern file to the build config:
:externs ["externs/react.js"]
When you run the release build again, Closure will preserve the React names. Verify by grepping the output for React – it should still appear.
3. Protect Your Own Symbols with ^:export
If you expose a function to the global namespace (e.g., for a test harness), mark it with ^:export:
(ns my.app
(:require [reagent.core :as r]))
(defn ^:export hello-world []
(js/console.log "Hello from ClojureScript"))
After compilation, the generated JavaScript will contain a line that registers helloWorld on the global this object, preventing it from being mangled.
4. Verify the Build
- Size comparison:
stat public/js/app.jsvsstat public/js/app-plain.js(a :whitespace build). You should see a 60‑90 % reduction. - Identifier inspection: Open the file and look for single‑letter function names. Ensure that any symbol you expect to be preserved (e.g.,
helloWorld) is present. - Runtime test: Serve the
publicfolder (e.g.,npx serve public) and open the page. If your React component renders correctly, the build is good. - Optional: generate a source map with
:source-map trueincompiler-optionsand use it to debug stack traces.
Trade‑offs & Limitations
- Dynamic property access: Closure cannot rename properties accessed via strings (e.g.,
obj["foo"]). Wrap such accesses withjs-objor add externs for the property names. - Debugging difficulty: Mangled names make stack traces unreadable. Use
:pretty-print trueor a source map for debugging. - Build time: Advanced mode adds significant compilation time (often 3‑5× longer). Use it only for production builds.
- Library support: Not all third‑party libs provide externs. You may need to write minimal externs or use
shadow-cljs’snpm-depsintegration, which auto‑generates externs for many popular libs.
Actionable Takeaway
To get the most from Closure’s advanced optimizations:
- Use
shadow-cljsorcljs.build.apito generate a :advanced build only for production. - Provide externs for every external global you reference.
- Mark your own exported symbols with
^:export. - Verify size, mangled names, and runtime behavior before deployment.
- Keep a lightweight :whitespace or :none build for local development and debugging.
With these steps, you can ship a ClojureScript app that is both tiny and reliable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.