Prevent Broken JavaScript Interop in ClojureScript Advanced Builds
Use externs files to protect external JavaScript names when compiling ClojureScript with :optimizations :advanced.
08 Oct 2025, 12:20 UTC

Problem and useful answer
Switching a ClojureScript build to :optimizations :advanced often makes the deployed JavaScript much smaller, but it can also break calls to jQuery, React, browser APIs, or any library loaded from a script tag. The compiler renames local symbols and removes code it thinks is unused. External names it does not know about are treated the same way.
The practical fix is an externs file: a plain JavaScript file that lists the global names the compiler must leave untouched. Use :advanced for production builds, keep :simple or :none for development, and verify every JavaScript interop path after the build.
How advanced compilation treats names
Advanced compilation performs whole-program dead-code elimination and symbol mangling. A function named send-data in your ClojureScript namespace might become a in the output. That is fine for code the compiler controls, but it is dangerous for names defined outside the compilation unit, such as jQuery, ReactDOM, or localStorage.
An externs file tells the compiler: do not mangle or remove these names. The compiler reads the externs alongside your source and treats the listed identifiers as external references.
Worked build configuration
Create a file named externs.js in your project root:
/* externs.js */
var jQuery;
var React;
var ReactDOM;
var localStorage;
/* jQuery example */
jQuery.prototype.ajax = function() {};
This declares that jQuery, React, ReactDOM, and localStorage exist at runtime. It does not define their behavior; it only prevents the ClojureScript compiler from renaming or dropping them.
Next, create build.edn:
{:optimizations :advanced
:externs ["externs.js"]
:output-to "target/main.js"
:main my.app
:pretty-print true}
Run the build from the project root in a terminal:
$ clojure -M -m cljs.main -co build.edn -c
Where to run: your project directory, using the Clojure CLI. Required permissions: read access to source and externs files, write access to the output directory (target/). Placeholders: replace my.app with your actual main namespace and target/main.js with your desired output path. Expected check: target/main.js is produced and contains your application code. Risk: an incorrect path in :externs can cause a build failure, while a missing extern entry causes a silent compile and a runtime failure later.
Safe and unsafe interop in the same build
Dot notation on a known property is generally safe because the compiler preserves the property name:
(.-localStorage js/window)
Computed access or bracket notation is not protected automatically:
(. js/window (getItem key)) ; key is a variable, not a literal name
If key holds "session-token", the compiler does not know that session-token must survive mangling unless the name appears in externs or you access it through a stable API.
For data coming from a server, do not rely on dot notation:
;; Risky under :advanced if the server key is unknown at compile time
(.-user-name data)
;; Safer: use the string key directly
(get data "user-name")
Limits and common mistakes
- Missing externs cause runtime errors. A build can succeed and still fail in the browser with errors such as a function being undefined, because the compiler renamed a global the page expected.
- Do not use :advanced for iterative development. Compilation is slower and source maps are less useful. Keep :advanced for release builds and use :simple during development.
- Externs are manual. If you upgrade a library and it adds or renames globals, update externs.js or the build may break in production only.
- Reflection and string-based access break. Libraries that look up properties by dynamically constructed strings need those strings listed in externs.
Verification
- Open
target/main.jsand search for a library global you declared in externs, such asjQuery. It should appear unchanged, not mangled. - Load the compiled page in a browser and exercise every interop path: DOM manipulation, AJAX calls, storage access, and third-party component initialization.
- Build the same project with
:optimizations :simpleand compare file sizes. A significantly smaller advanced build confirms dead-code elimination is active.
If something fails, the practical rollback is to switch :optimizations back to :simple, rebuild, and redeploy the previous working artifact while you fix the externs file.
Limitation
Externs protect names you know about in advance. They cannot protect keys received dynamically from JSON responses or user input. Treat externs as a supplement to browser testing, not a replacement for it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.