engineeringtooling 12 min read

The Long Road to Whole-Program Compilers

July 9, 2026 · 04:06 UTC

The Long Road to Whole-Program Compilers

Opening the page source of Google search one quiet night started this, because the CSS and JavaScript there looked unlike a normal build, rewritten into a dense stream of short tokens that stayed unique, minified, and fast while hiding each name.

Most build tools already shrink CSS and JavaScript on their own, cutting bytes and renaming local things inside a file, yet none reach across languages to rename a class and then carry that exact change into every string and attribute pointing back to it, which is the one missing move that turned a single night of plain curiosity into eleven very long months of work.

That gap, holding one name aligned across three languages at once, is why the work felt like crossing a field of quiet landmines.

One Name, Three Languages

A class name looks harmless until it becomes clear that three languages each keep a copy. The stylesheet declares the rule, the markup wires it onto an element, and the script toggles it while running, each holding one string with no shared link.

/* CSS declares the rule */
.filter-active {
  opacity: 1;
}
<!-- HTML attaches the class -->
<button class="filter-active">All</button>
// JS flips it at runtime
element.classList.toggle('filter-active')

Renaming only the stylesheet to .dM silently breaks the other two, since neither of them was ever consulted during the rewrite. A whole-program compiler then rewrites all three of them from one shared map, so the class lands as dM in every spot at once, and the running page behaves exactly as before while shipping far fewer bytes than the readable names once cost.

Missing even a single usage site raises no error at all, so the console stays clean while it repaints the whole page a bit wrong.

Selectors Stop Behaving

An early plan sounded clean, walk the stylesheet, gather each selector, hand out tokens, then patch them into markup and script. Reality had other ideas, since a selector stops staying plain once custom properties, keyframes, and attributes arrive.

Custom properties demanded the same discipline as the class names here, because a renamed --iq in one place has to trace back to its own declaration everywhere that it gets read, and one missed reference floods the whole page with the wrong color.

/* Every reference has to follow its renamed declaration */
--iq: #0b0b0b;
.panel {
  color: var(--iq);
}

Keyframe names had to stay untouched on purpose, because an animation links by a plain string inside animation: pulse ..., and rewriting it halts the motion with no warning. The compiler had to learn what was safe to touch and what needed cover.

Tokens Line Up

Proof that the pipeline had truly gone whole-program came from watching the script strings line up with the selectors right after a build. Classes toggled inside the shipped scripts arrived as the exact same tokens already waiting in the stylesheet.

// After the build, these tokens match the CSS selectors exactly
element.classList.add('dM')
element.classList.remove('cn')
element.classList.toggle('fy')

Holding that alignment meant the compiler had to parse the script, spot which strings were class names inside a classList call, and rewrite them from the map that renamed the selectors. Method names obeyed a parallel rule, public ones reached from other modules stayed readable while private ones got mangled, which holds once the call graph is known across every file.

Eight class names matching eight selectors, with nothing edited by hand, was the first moment the long months began paying off.

Engines Stay Apart

Coordinated whole-program renaming is not new, it has run for over a decade inside compilers built at large companies, all public.

Its absence from general tooling is structural rather than a matter of effort or of raw skill. Styles, scripts, and templates each pass through separate engines that were all built to stay independent, and every one runs blind to the others and often at the very same time, so a single shared rename map spread across them turns into a silent race instead of a plan.

Keeping those duties apart is what lets a general tool stay safe across countless different codebases. There is also the opaque-string problem, where a tool cannot tell a class name from a plain string, and a wrong guess breaks a page in silence.

So the technique tends to live where one pipeline can see every layer at once, which is exactly the corner this build ended up in.

One Pipeline Sees All

Feasibility arrived only once a few things lined up inside one pipeline. The first was a single compiler reading styles, markup, and scripts against one shared symbol map instead of three separate engines that never talk. The second was keeping class names in places the compiler can actually find, so they never get stitched together from scattered runtime fragments.

Even so, the stack leans on third-party modules and its own custom JavaScript, so this was never about avoiding dependencies, it was about one pipeline holding the whole map in view. That reach let it cover every class, tested on Tailwind CSS v4.3.2.

Failures Hide in Silence

Every condition met, the build still stayed brutal for a long stretch, because the compiler had to enumerate each class usage across selectors, attributes, literals, and templates, then keep them all in lockstep with not one reference left behind.

It had to split real class strings from ordinary ones, track var(), :has(), :is(), and [class*=], and step around class systems that a runtime highlighter builds on its own rather than at build time. Every one of those was a small trapdoor, and the failure mode never changed, a clean console, a page that loads fine, and styling quietly off with no trace to follow.

Eleven months of that, handled in slow stages tucked between everything else on the plate, is why the work earned the word hell without any exaggeration at all, since it forgave nothing and buried each fresh mistake until a hand finally dug it out.

Foreign Code Stays Whole

Trust on hand-written files came cheap, since every name in them already sat in plain view. Real proof lived somewhere harder, inside the shipping builds the whole web quietly runs, each one squeezed by a different toolchain full of dark corners.

So the sweep turned into a slow audit across dozens of those builds, each one pulled down, passed through the same rename and shrink, then read back with a fresh parser to confirm nothing had shifted. A single dropped token would have failed the way every earlier bug once did, no crash at all, just a page loading fine while running subtly wrong with no trace to follow.

LibraryOriginalOptimizedChangeResult
lodash7323473085-0.2%reparse + v8 ok
jquery8753387835+0.3%reparse + v8 ok
react1075110530-2.1%reparse + v8 ok
react-dom131835131870+0.0%reparse + v8 ok
axios6240662504+0.2%reparse + v8 ok
moment6065760335-0.5%reparse + v8 ok
three669884673207+0.5%reparse + v8 ok
d3279703280763+0.4%reparse + v8 ok
chart.js208522209861+0.6%reparse + v8 ok
bootstrap8049680896+0.5%reparse + v8 ok
rxjs8806087206-1.0%reparse + v8 ok
zod17775469066-61.1%reparse + v8 ok
date-fns9856299233+0.7%reparse + v8 ok
dayjs71617214+0.7%reparse + v8 ok
ethers526548403231-23.4%reparse + v8 ok
dompurify2843828346-0.3%reparse + v8 ok
lottie-web305704eval guard, left intact
htmx51238eval guard, left intact
katex277038277576+0.2%reparse + v8 ok
swiper154597154598+0.0%reparse + v8 ok

Most of them barely changed in size, and that was the quiet point. Those files arrived already wrung dry by their own build, so a second pass found little slack to reclaim, and the rare ones shrinking hard were the few still shipping readable names.

A couple refused the pass on purpose. Each leans on runtime string evaluation, feeding itself names a rename would slide out from under it, the exact opaque-string trap haunting this whole idea. Rather than gamble on a page that loads clean but behaves wrong, the compiler steps back and leaves them whole, since knowing which name to spare is its own quiet kind of correct.

Five Stages Under One Roof

Symbol renaming was never a lone trick bolted onto a build, it grew inside a toolchain already carrying five stages at once, folding markdown into content, resolving a template engine, deriving semantics, wiring assets, then emitting the mangled result. Each stage feeds the next, so the shared map has every layer agreeing on what a class means before a token ever shrinks.

Beyond that, its wider machine lives in a separate write-up on architecting a static site generator, where the full pipeline behind the compiler finally earns some real room to breathe across every one of its five distinct stages, one after another.

Stranger still was how the whole thing became something worth returning to again and again across the long months of building it. Once a single stage finally clicked into place, the quiet pull toward refining the next one arrived entirely unprompted, and a humble build tool meant only to serve the site slowly turned into the most rewarding corner of the entire project.

Layers Share One Name

The finished pipeline now renames styles, markup, and scripts together from one shared source of truth, ships dense tokens that load fast, and holds every last reference aligned without any manual pass. The hard part was never the renaming itself, it was giving CSS a clear place inside a pipeline that already spoke both markup and scripts, one shared map for all.

In the end this write-up is a quiet note left after eleven months on how those three layers finally learned to share one name.

Share
On this page
No results found.