Ship Smaller ClojureScript Bundles with :advanced Without Breaking JavaScript Interop
Use ClojureScript :advanced compilation for smaller release bundles while protecting JavaScript APIs with externs and verifying every interop boundary before deploy.
03 Dec 2025, 18:51 UTC

The problem: small bundles that break at runtime
ClojureScript’s :advanced optimization level removes unused code and renames symbols to shrink release JavaScript. The same renaming can rename JavaScript properties your code touches, so a build that compiles cleanly can fail in the browser with errors such as "undefined is not a function." :none and :simple preserve original names, which makes them safer during development but larger in production.
Useful takeaway: ship :advanced for release, but protect every JavaScript API surface with externs or compiler-aware interop, then exercise each boundary in the built artifact before you deploy.
Prerequisites
- A ClojureScript project using a deps.edn-based build or cljs.build.api (assumes ClojureScript 1.11.x; confirm your project’s exact compiler version).
- Java installed and on PATH.
- One or more JavaScript interop points in your source, such as console, DOM APIs, or a foreign library.
- Write access to the project’s build configuration and source directories.
Configure the build for :advanced
Add or edit a build map that sets :optimizations to :advanced and lists an externs file. The following example uses cljs.build.api; equivalent flags exist in shadow-cljs and cljs.main, and flags vary by tool and compiler version.
;; build.edn - example configuration
{:main my.app
:output-to "target/main.js"
:optimizations :advanced
:externs ["externs.js"]}
Run the build from the project root in a terminal. No special permissions are required beyond normal file access.
clojure -M:dev -e "(require 'cljs.build.api)
(cljs.build.api/build "src"
{:main 'my.app
:output-to "target/main.js"
:optimizations :advanced
:externs ["externs.js"]})"
Replace my.app, src, and externs.js with your project’s values. Compilation takes longer than :none or :simple.
Protect JavaScript names with externs
Externs tell the compiler which JavaScript names must not be renamed. Without them, :advanced may mangle a global object or method and the compiled code will fail at runtime.
Create externs.js in the project root. This example protects a hypothetical charting library and a custom global:
// externs.js
var myChart = {};
myChart.init = function() {};
myChart.update = function() {};
var __MY_CONFIG__ = {};
__MY_CONFIG__.apiEndpoint = "";
Then reference those names from ClojureScript using compiler-aware interop:
;; src/my/app.cljs
(ns my.app)
(defn ^:export start []
(js/console.log "application started")
(let [chart (js/myChart.init)]
(when chart
(.-update chart 42)))
(js/__MY_CONFIG__.apiEndpoint))
Note: js/console.log is compiler-aware and usually safe, but custom globals such as myChart need externs under :advanced.
Verify the compiled output
After building, inspect the emitted JavaScript to confirm protected names survived mangling.
- Run grep -o "myChart\.init" target/main.js. A match means the externs worked; no match may mean the name was mangled or removed.
- Open the project’s HTML page in a browser and open DevTools.
- Trigger every interop path: console output, chart initialization, config reads, DOM events.
- Watch the console for TypeError, "undefined is not a function," or missing-property errors.
- Compare bundle size between optimization levels using ls -lh target/main.js.
Run the same build with :optimizations :simple to a different output path and compare file sizes. :advanced usually produces a smaller bundle, but the benefit depends on how much JavaScript interop your code retains.
Expected result
The page loads without interop-related console errors, protected JavaScript names appear unchanged in the bundle, and the :advanced artifact is equal to or smaller than the :simple artifact.
Limitations and recovery
- Externs must cover every JavaScript API surface your ClojureScript code touches. A single missing property can compile successfully and fail only in the browser.
- :advanced compile times are longer than :none or :simple, which can slow iterative builds.
- Compiler behavior and externs requirements vary across ClojureScript releases. Verify against the exact version in your project.
- Do not serve :advanced output alongside external scripts that expect unmangled ClojureScript symbol names without integration testing.
If runtime errors appear, revert :optimizations to :simple, redeploy the previous working artifact, add the missing externs, and retry :advanced in a staging environment before promoting to production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.