Using ClojureScript Advanced Compilation with Externs for Optimized Frontend Builds
Learn how to define requirements, create the smallest viable ClojureScript build with :advanced optimizations and externs, enforce trust boundaries, run operational checks, anticipate failure modes, and know when to adjust the design.
16 Feb 2026, 10:06 UTC

Requirements
Before enabling :advanced optimizations you must know exactly which JavaScript libraries the ClojureScript code will call. For each library you need an externs file that lists the global symbols (functions, objects, properties) that should retain their original names after compilation. The externs can be hand‑written, generated with tools like gulp-closure-externs, or taken from community repositories.
The build tool must support the Google Closure Compiler’s :advanced mode. Popular choices are shadow-cljs (via :optimizations :advanced) or lein-cljsbuild with the :advanced profile.
Smallest Suitable Design
A minimal viable setup consists of:
- One ClojureScript namespace (
app.core) that contains all UI logic and interacts with the external JS through the externs. - An externs file (
react-externs.js) that declares the React API you use (e.g.,React.createElement,React.useState). - A
shadow-cljs.ednconfiguration that enables :advanced and points to the externs:
{:source-paths ["src"]
:builds {:app {:target :browser
:output-dir "public/js"
:asset-path "js"
:optimizations :advanced
:externs ["externs/react-externs.js"]
:modules {:main {:entries [#'app.core]}}}}}
The HTML page loads the generated public/js/main.js (gzipped in production) and a tiny CSS bundle.
Trust/Data Boundaries
The externs file is the contract between ClojureScript and the JavaScript world. Any symbol not listed in the externs is considered opaque: the Closure Compiler may rename it, drop it, or inline it. Therefore:
- Data passed from ClojureScript to JavaScript must only use properties that appear in the externs (e.g., calling
(.useState React 0)is safe ifuseStateis declared). - If you need to access a property that is not in the externs, you must either add it to the externs or use a string‑based access (
(.-myProp jsObj)) which prevents renaming but defeats some optimizations. - Conversely, JavaScript code that calls into ClojureScript must refer to the exported names exactly as they appear in the externs (the compiler will not mangle those).
Operational Checks
- Source maps – Enable
:source-map trueduring development to trace mangled names back to the original ClojureScript. - Externs validation – After an :advanced build, inspect the generated JS (e.g.,
grep -E 'React\.useState' public/js/main.js) to confirm that every symbol listed in the externs appears unchanged. - Linting – Run the output through
google-closure-linteroreslintwith the Closure Compiler plugin to catch accidental renames. - Size verification – Compare the gzipped size of the :advanced build with a :whitespace build (
gzip -c public/js/main.js | wc -c). A reduction of at least 40 % indicates the optimization is effective. - Runtime test – Load the page in a browser and open the console; absence of errors like "undefined is not a function" or "React is not defined" suggests the externs contract holds.
- Bidirectional unit tests – Execute your test suite twice: once with :none optimizations (to ensure logic is correct) and once with :advanced (to catch externs mismatches early).
Failure Modes
- Missing extern entry – If a called JavaScript method is omitted, the compiler may rename it, producing a runtime "undefined" error that is hard to trace without source maps.
- Incorrect type annotations – Externs only preserve names; they do not enforce types. Passing an unexpected shape can still cause JS errors.
- Aggressive dead‑code elimination** – The Closure Compiler may remove entire library calls it believes are unused. If your code calls a library indirectly (e.g., via a string‑based property), the call can be dropped, leading to silent failures.
- Build time increase** – :advanced mode can be 5‑10× slower than :whitespace, impacting feedback loops.
Conditions That Would Change the Design
You would reconsider the :advanced + externs approach if:
- Your project relies heavily on dynamic JavaScript interop (e.g., jQuery plugins accessed via string keys) where maintaining a complete externs file becomes impractical.
- Build performance is a critical constraint and incremental compilation is required; in that case you might stay at :whitespace or :simple and rely on HTTP caching rather than size reduction.
- The team lacks expertise to maintain externs correctly, and the risk of runtime errors outweighs the bundle‑size gains.
- You are targeting environments where the Closure Compiler’s advanced optimizations are unsupported (e.g., certain CSP‑restricted contexts that prohibit eval‑like behavior).
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.