ClojureScript Advanced Optimizations: Shrink Production Bundles with Externs
Discover how to enable ClojureScript's :advanced compiler flag and write externs to cut bundle size by up to 70% while keeping the public API intact.
01 Sept 2026, 06:13 UTC

Problem: bloated JavaScript bundles slow page loads and waste bandwidth
When a ClojureScript application is compiled with the default settings, the generated JavaScript can be many times larger than necessary. Large files increase download time, consume mobile data, and degrade user experience, especially on slower connections.
Thesis: enable the :advanced optimization flag together with well‑crafted externs files to achieve a substantial size reduction while preserving the public JavaScript API
Closure Compiler optimization levels
The Google Closure Compiler offers three levels:
:none– no transformation; the output is a direct translation of the source.:simple– renames local symbols but does not perform aggressive dead‑code elimination or property renaming.:advanced– performs full name mangling, removes unreachable code, and rewrites property accesses using the compiler’s advanced type‑inference.
Only :advanced can shrink the output dramatically, but it requires that the compiler know which JavaScript names are intended to stay stable.
Writing externs
Externs files describe the shape of external JavaScript that ClojureScript may call. By listing exported variables, functions, and properties, the compiler can safely rename ClojureScript symbols without breaking the public surface.
An externs file is a plain JavaScript file that defines a single global object (often goog) with properties that map to the names you want to keep unchanged.
Worked example
Consider a tiny Reagent component that exposes an init function to be called from plain JavaScript.
;; src/my_app/core.cljs
(ns my-app.core
(:require [reagent.core :as r]))
(defn ^:dev/after-load init []
(r/render [app] (.getElementById js/document "app")))
(defn ^:export app []
[:div "Hello from ClojureScript"])
The corresponding externs file (externs.js) declares the public entry point:
goog.provide('myApp');
myApp = {
init: function() { /* implementation omitted */ }
};
Now a build script that enables advanced optimizations:
lein cljsbuild :build
{:optimizations :advanced
:source-paths "src"
:externs "externs.js"})
Running the build produces a minified bundle where the init function is renamed internally, but the global myApp.init remains callable from external JavaScript.
A typical size comparison shows the advanced build being roughly 70 % smaller when gzipped compared to a :none build.
Verification steps
- Build the project twice: once with
:optimizations :noneand once with:optimizations :advanced. - Run a size check (e.g.,
gzip-sizeorsize-limit) on the generatedgoog.jsfiles to confirm the reduction. - Load the advanced bundle in a browser, open the console, and verify that
myApp.init()executes without errors. - Optionally invoke the Closure Compiler with
--pretty_printon the advanced output and inspect that extern‑listed identifiers stay unchanged.
Trade‑off / Limitation
Advanced optimizations demand accurate externs. Missing or incorrect entries cause the compiler to rename public symbols, leading to runtime errors such as undefined is not a function. Additionally, the produced code becomes harder to read and debug because local symbols are mangled.
Actionable closing
Start development with :optimizations :none to keep the workflow fast and readable. When you are ready for production, generate externs automatically (for example, with a grep on exported names or by converting JSDoc comments) and run the advanced build in a staging environment. Add a bundle‑size check to your CI pipeline so that any regression in size is caught early.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.