Optimizing compilers
Learn how JavaScript optimizing compilers build SSA, graphs, engine pipelines, and background threads that turn hot code into fast machine code.
- 01Read an optimizer pipelineExplain why engines translate hot JavaScript into intermediate representations before generating machine code.
- 02Use SSA and graph vocabularyDescribe single static assignment, phi nodes, control-flow graphs, and Sea-of-Nodes graphs with a tiny function.
- 03Compare real engine designsPlace V8 Maglev, TurboFan, Turboshaft, SpiderMonkey WarpMonkey, and JavaScriptCore DFG, FTL, and B3 in the right roles.
Optimizers build a second program
A fast JavaScript engine does not turn hot code straight from source text into final machine code. It first builds an intermediate representation, or IR: a compiler-friendly version of the program. The IR lets the compiler name values, connect branches, attach feedback, and choose machine instructions without losing JavaScript's meaning.
An optimizing compiler is a JIT tier that spends extra compile time on hot JavaScript, builds one or more IR graphs, specializes those graphs with runtime feedback, and emits faster machine code while preserving the same JavaScript result.
The Baseline compilers lesson explains quick tiers such as Sparkplug. This lesson starts after that point: how V8's Maglev and TurboFan, SpiderMonkey's WarpMonkey, and JavaScriptCore's DFG and FTL build optimizer pipelines. The next lesson, Speculation & deoptimization, zooms in on guards and bailouts, so here we focus on IRs, SSA, graphs, pipeline stages, and compiler threads.
IR and SSA from zero
SSASingle static assignment, or SSA, means each value name in the compiler IR is assigned exactly once. JavaScript variables can still change. The compiler simply rewrites those changes into new internal names so later passes can ask “which exact value does this use?”
function finalPrice(isMember) { let discount = 5; if (isMember) { discount = 20; } return 100 - discount;} console.log(finalPrice(true));console.log(finalPrice(false));Line 2 gives discount a regular value of 5. Line 4 may replace it with 20. Line 6 reads whichever value is current for the path that ran. The program prints 80 for a member and 95 for a non-member.
A cook fills each labeled lunch box once and leaves its label in place. If two boxes can reach the same table, the table uses the one that arrived. SSA gives compilers the same clear record.
- In real life: Each lunch box gets its own label
- In JavaScript: Each SSA value gets a fresh name such as
discount0 - In real life: A filled box is not emptied and reused
- In JavaScript: An SSA name is assigned once, then only read
- In real life: Two boxes arrive at the same table
- In JavaScript: A phi node chooses the branch value that arrived
- In real life: The table uses the box that arrived
- In JavaScript: Later code reads one merged value,
discount2
Where the analogy stops: Lunch boxes hold food. SSA names hold compiler values, and a real engine has many more nodes and guards.
Step through the converter. It is a small teaching model, but it really creates the SSA strings shown beside the code.
Step through a real, tiny SSA converter. This is a teaching model for compiler IR construction, not a dump from V8.
script
function convertDiscountToSsa() { const nextVersion = {}; function name(base) { const version = nextVersion[base] ?? 0; nextVersion[base] = version + 1; return base + version; } const total = name("total"); ssa.push(total + " = parameter total"); const regularDiscount = name("discount"); ssa.push(regularDiscount + " = 5"); const memberDiscount = name("discount"); ssa.push(memberDiscount + " = 20"); const mergedDiscount = name("discount"); ssa.push(mergedDiscount + " = phi(" + memberDiscount + ", " + regularDiscount + ")"); const payable = name("payable"); ssa.push(payable + " = " + total + " - " + mergedDiscount); return ssa;} console.log(convertDiscountToSsa().join(" | "));Maglev: a fast SSA compiler
V8V8's Maglev post describes Maglev as a simple SSA-based JIT compiler introduced in Chrome M117. It sits between Sparkplug and TurboFan: stronger than the baseline compiler, but designed to build good-enough optimized code faster than the top tier.
const feedback = { shape: "CartShape", priceType: "smi" }; function maglevPlan(feedback) { const graph = []; graph.push("CheckShape " + feedback.shape); graph.push("LoadField price"); graph.push("Int32Add"); graph.push("Return"); return graph;} console.log(maglevPlan(feedback).join(" -> "));Line 1 stands in for runtime feedback gathered by earlier tiers. Lines 5 and 6 model a shape check followed by a field load. Real Maglev uses bytecode, feedback, liveness information, and a CFG-shaped SSA graph. The model only gives you the vocabulary.
| Compiler | Role | How to think about it |
|---|---|---|
| Maglev | Fast SSA compiler | Builds a compact CFG-shaped SSA graph from bytecode and feedback, then emits good-enough optimized code quickly. |
| TurboFan | Top-tier optimizer | Spends more compile time for peak V8 performance; its JavaScript backend now uses Turboshaft for the backend path. |
| Turboshaft | CFG-shaped backend IR | A newer V8 IR that replaces much of TurboFan's old Sea-of-Nodes backend with a more traditional control-flow graph shape. |
| WarpMonkey | SpiderMonkey optimizer | Uses bytecode plus inline-cache data, including CacheIR snapshots, to build MIR and optimized code. |
| FTL with B3 | JavaScriptCore top tier | Combines JSC's high-level optimizer with the B3 low-level backend for hot code. |
The lesson test checks the local Node 22 / V8 12.4 build with --v8-options. It proves --maglev defaults to --no-maglev in Node, while TurboFan and concurrent recompilation are enabled. A Maglev native probe with the flag still reports “Maglev is not enabled” in this Node build, so the real trace evidence below uses TurboFan.
TurboFan, Turboshaft, and graph shapes
GRAPHSTurboFan is V8's top-tier optimizing compiler. Historically it was famous for using a Sea of Nodes IR, where nodes are connected by value, effect, and control dependencies instead of being fixed inside blocks from the start. V8's 2025 “Land ahoy: leaving the Sea of Nodes” post says the JavaScript backend now uses Turboshaft, a more traditional control-flow-graph-shaped IR.
function chooseBonus(a, b, flag) { const left = a * 2; const right = b * 2; const picked = flag ? left : right; return picked + 1;} console.log(chooseBonus(3, 4, true));console.log(chooseBonus(3, 4, false));- Block entry: compute left, compute right, branch on flag
- Block then: picked is left
- Block else: picked is right
- Block merge: return picked + 1
The program still prints 7 and 9. A compiler IR is a tool for the compiler, not a new JavaScript meaning.
Control-flow graph: A CFG groups instructions into blocks and connects those blocks with control-flow edges. It is easy to read in execution order. This is a teaching model; it does not inspect your browser's engine.
In the playground, line 2 and line 3 can be reasoned about independently because neither changes JavaScript-visible state. A CFG view shows basic blocks and branches. A Sea-of-Nodes view shows the value dependencies that let pure work float until scheduling.
| Representation | Core shape | Why a compiler uses it |
|---|---|---|
| Control-flow graph | Basic blocks plus edges for branches and jumps. | Excellent when compiler passes want an explicit execution order. |
| Sea of Nodes | Nodes mostly connected by value, effect, and control dependencies. | Excellent when passes want pure operations to float until scheduling. |
| Turboshaft direction | V8's JavaScript backend moved toward a CFG-shaped IR. | Keeps many optimization benefits while improving compiler engineering and cache behavior. |
function chooseBonus(a, b, flag) { const left = a * 2; const right = b * 2; const picked = flag ? left : right; return picked + 1;} console.log(chooseBonus(3, 4, true));console.log(chooseBonus(3, 4, false));- V8 Maglev describes Maglev as a simple SSA-based JIT compiler in Chrome M117.
- V8 leaving the Sea of Nodes describes Turboshaft and the shift toward CFG-shaped IRs.
WarpMonkey: CacheIR to MIR and trial inlining
SPIDERMONKEYSpiderMonkey's optimizer is WarpMonkey. Mozilla's source docs say Baseline tiers attach inline caches, and CacheIR is the core format for those speculative cache stubs. When Warp optimizes, WarpOracle snapshots bytecode and relevant IC data. WarpBuilder and the WarpCacheIRTranspiler turn that CacheIR into MIR, SpiderMonkey's middle-level IR.
const cacheIR = ["GuardShape", "LoadFixedSlotResult"]; function warpToMir(cacheIR) { return cacheIR.map((op) => { if (op === "GuardShape") return "MGuardShape"; if (op === "LoadFixedSlotResult") return "MLoadFixedSlot"; return "MUnsupported"; });} console.log(warpToMir(cacheIR).join(" -> "));Line 1 is the inline-cache evidence: guard the shape, then load a fixed slot. Lines 4 through 6 translate those cache operations into MIR-shaped names. Real Warp also uses call information for trial inlining, where hot callees are tried inside the caller before the optimizer commits to final code.
The Firefox CacheIR docs describe CacheIR stubs, snapshots, and WarpCacheIRTranspiler. The SpiderMonkey overview names WarpMonkey as the highest optimization tier and says it translates bytecode and inline-cache data into MIR, then LIR and code.
JavaScriptCore: DFG, FTL, and B3
JSCJavaScriptCore uses different names. The DFG JIT is a high-level optimizing compiler. The FTL JIT is a stronger top tier. WebKit's B3 post explains that FTL replaced LLVM with the Bare Bones Backend, or B3, as its low-level optimizing backend.
const jscTiers = ["Baseline JIT", "DFG", "FTL", "B3"]; function describeJscPipeline(tiers) { return tiers.join(" -> ");} console.log(describeJscPipeline(jscTiers));This code only prints names, but it keeps the stack straight: Baseline JIT feeds hot code to DFG, FTL is the top tier, and B3 is the low-level backend used by FTL. The exact heuristics are engine details, not JavaScript semantics.
| Engine | Mid optimizer | Top/back-end path | Important caution |
|---|---|---|---|
| V8 | Maglev | TurboFan with Turboshaft backend work | Ignition and Sparkplug feed feedback and bytecode; Node 22's V8 12.4 build keeps Maglev off by default. |
| SpiderMonkey | WarpMonkey | MIR lowered to LIR | CacheIR stubs from Baseline tiers are snapshotted and transpiled to MIR; trial inlining uses observed calls. |
| JavaScriptCore | DFG | FTL using B3 | DFG performs high-level optimizations; FTL uses B3 as a low-level optimizing backend. |
WebKit's B3 post says FTL uses DFG for high-level optimization and B3 as the FTL backend. Its speculation post explains why these tiers specialize dynamic JavaScript but must be ready to recover when assumptions fail.
Concurrent compilation
THREADSConcurrent compilation means the engine can build optimized code on a background thread while the main thread keeps executing older code. The compiler works from snapshots, feedback, and dependencies; the runtime installs the new code only when it is ready and safe.
Lunch keeps being served while one cook plans a faster order for the dishes. When the plan is ready, the kitchen can use it without changing the meal.
- In real life: Lunch keeps reaching the table
- In JavaScript: The main thread keeps running baseline code
- In real life: A cook plans a faster serving order
- In JavaScript: The optimizer builds code on a background thread
- In real life: The new order starts when ready
- In JavaScript: Optimized code is installed after compilation completes
- In real life: The meal stays the same
- In JavaScript: JavaScript results must stay identical across tiers
Where the analogy stops: A cook can pause a meal. A JavaScript engine must preserve observable behavior and cannot expose compiler threads to your code.
Replay a conceptual concurrent-compilation timeline. It models thread roles; it does not inspect your browser's engine.
script
function buildConcurrentTimeline(callsBeforeReady = 3) { const events = []; events.push("main: run baseline call 1"); events.push("background: compile optimized graph"); for (let call = 2; call <= callsBeforeReady; call += 1) { events.push("main: run baseline call " + call); } events.push("main: install optimized code when it is ready"); events.push("main: run optimized call " + (callsBeforeReady + 1)); return events;} | Part | Job | Why it matters |
|---|---|---|
| Main thread | Keeps running baseline or older optimized code. | Responsiveness matters more than blocking for a perfect compile. |
| Background compiler | Builds the optimized graph and machine code. | It must use snapshots and guards because the program keeps changing. |
| Install point | Switches to optimized code when ready and safe. | The JavaScript result must be identical before and after the switch. |
function hotSum(x) { let total = 0; for (let i = 0; i < 200; i += 1) total = (total + x + i) | 0; return total;} for (let n = 0; n < 20000; n += 1) hotSum(n);Default:
[compiling method ... hotSum ... (target TURBOFAN), mode: ConcurrencyMode::kConcurrent]
With --no-concurrent-recompilation:
[compiling method ... hotSum ... (target TURBOFAN), mode: ConcurrencyMode::kSynchronous]
With --allow-natives-syntax --maglev:
Maglev is not enabled.// Node-only probe. Run with: node --v8-options
// The lesson tests assert stable substrings only:
// --maglev defaults to --no-maglev in Node 22's V8 12.4 build.
// --turbofan and --concurrent-recompilation are enabled by default.The tests assert stable substrings only: (target TURBOFAN), ConcurrencyMode::kConcurrent by default, and ConcurrencyMode::kSynchronous when Node runs with --no-concurrent-recompilation. They do not assert timings, memory addresses, or exact patch versions.
Use this knowledge safely
Optimizing compilers explain why warm-up exists and why a small source change can affect hot-code performance. They are not a checklist of private knobs to force. In application code, write clear JavaScript, keep data shapes consistent where it is natural, and measure the user-visible path before changing code for performance.
- Warm up microbenchmarks, then report repeated samples instead of trusting the first cold run.
- Use browser performance tools to find the hot interaction before thinking about compiler internals.
- Prefer stable, readable data flow over clever code written for one named tier.
- Link optimizer surprises to the next lesson on speculation and deoptimization.
- A variable rewrite where
discount0,discount1, and a phi replace repeated writes. - A V8 backend path that moves away from the old Sea-of-Nodes backend toward a CFG-shaped IR.
- SpiderMonkey inline-cache records that Warp can transpile into MIR.
- Build optimized code while the main thread continues running older code.
- JavaScriptCore's low-level backend used by the FTL tier.
Put each card under the kind of idea it describes.
Common misconceptions
“SSA means my JavaScript variables cannot change.”
No. SSA is the compiler's internal naming system. Your JavaScript variables keep their normal semantics.
“Sea of Nodes and CFGs are just diagrams.”
They are IR design choices that affect which compiler passes are easy, fast, or cache-friendly to run.
“Maglev is always visible in Node if the flag exists.”
The tested Node 22 / V8 12.4 build lists the flag but reports Maglev disabled for native Maglev optimization.
“Concurrent compilation makes JavaScript run on a worker thread.”
The compiler job can be off-thread. Your function's JavaScript execution still follows the language's normal threading model.
| Term | Means | Does not mean |
|---|---|---|
| IR | A compiler's internal program representation. | A JavaScript feature you can observe directly in normal code. |
| SSA | A naming discipline where each value name is assigned once. | A promise that JavaScript variables cannot change. |
| Phi node | A merge value at a control-flow join. | A runtime function JavaScript calls. |
| Concurrent compilation | Compilation work can happen off the main thread. | Your function runs in two different threads at once. |
Practice exercises
6 EXERCISESWhat number prints for the member path?
function finalPrice(isMember) {
let discount = 5;
if (isMember) discount = 20;
return 100 - discount;
}
console.log(finalPrice(true));console.log(100 - 20);finalPrice(true) takes the member branch, so it prints 80.
Which SSA idea merges the member and regular discount values after the branch?
A phi node merges incoming SSA values. Here discount2 = phi(discount1, discount0) gives later code one value to read.
Which representation groups instructions into basic blocks?
A control-flow graph groups instructions into basic blocks and connects those blocks with branch edges.
Warp transpiles successful CacheIR snapshots into which middle representation?
WarpBuilder and WarpCacheIRTranspiler turn CacheIR snapshots into MIR, then SpiderMonkey lowers MIR to LIR.
The starter timeline installs optimized code too early. Describe the correct ordering.
const timeline = [
"main: install optimized code",
"background: compile optimized graph"
];
console.log(timeline.join(" -> "));const timeline = [
"background: compile optimized graph",
"main: install optimized code"
];
console.log(timeline.join(" -> "));Optimized code can be installed only after the compiler job has produced it and the runtime decides it is safe.
A checkout page feels slow after a refactor. What should you do before rewriting code for Maglev, TurboFan, Warp, or FTL?
Measure first. Use user-visible performance tools, warm up targeted benchmarks, and keep code clear before making optimizer-motivated changes.
Quiz: check your understanding
8 QUESTIONSUse the IR vocabulary, engine pipeline names, and thread model from the lesson.
Question 1 of 8Why do optimizing compilers build an IR before machine code?
Choose an answer to see the explanation.
Question 2 of 8What does this branch example print first?
Read the code, then predictfunction finalPrice(isMember) { let discount = 5; if (isMember) discount = 20; return 100 - discount; } console.log(finalPrice(true));Choose an answer to see the explanation.
Question 3 of 8In SSA, what does a phi node do?
Choose an answer to see the explanation.
Question 4 of 8Which statement matches Maglev as described by V8?
Choose an answer to see the explanation.
Question 5 of 8What does this timeline model print last?
Read the code, then predictconst events = []; events.push("main: baseline call"); events.push("background: compile"); events.push("main: install optimized"); console.log(events.at(-1));Choose an answer to see the explanation.
Question 6 of 8How does V8's 2025 Turboshaft direction relate to Sea of Nodes?
Choose an answer to see the explanation.
Question 7 of 8What does SpiderMonkey's Warp pipeline use CacheIR for?
Choose an answer to see the explanation.
Question 8 of 8What is the safest production lesson from optimizer internals?
Choose an answer to see the explanation.
Key takeaways
- Optimizing compilers build internal IRs so they can reason about values, control flow, effects, feedback, and machine code.
- SSA assigns each compiler value name once; phi nodes merge values from different control-flow paths.
- V8 uses Maglev for fast SSA optimization and TurboFan/Turboshaft for top-tier backend work; Node 22's V8 12.4 build keeps Maglev off by default.
- SpiderMonkey's Warp uses CacheIR and MIR; JavaScriptCore uses DFG, FTL, and the B3 backend.
- Concurrent compilation lets optimized code be built off-thread while older code keeps running on the main thread.
Remember the one-liner.
Optimizing compilers are careful translators: they rebuild hot JavaScript as graphs, optimize those graphs, and swap in faster code only when the same result is protected.
Up next: Speculation & deoptimization, where those protected assumptions become guards and deopt exits.