cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Optimizing compilers

Learn how JavaScript optimizing compilers build SSA, graphs, engine pipelines, and background threads that turn hot code into fast machine code.

By the end, you can
  • 01
    Read an optimizer pipelineExplain why engines translate hot JavaScript into intermediate representations before generating machine code.
  • 02
    Use SSA and graph vocabularyDescribe single static assignment, phi nodes, control-flow graphs, and Sea-of-Nodes graphs with a tiny function.
  • 03
    Compare 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.

Definition

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

SSA

Single 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?”

The JavaScript value changes normallyPop out in the code editor (opens in a new tab)JavaScript
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.

Real-life analogyLabeled lunch boxes filled once

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.

SSA walkthrough: rename repeated assignments
Step 0 of 13Ready
Your turn: follow the blue line

Step through a real, tiny SSA converter. This is a teaching model for compiler IR construction, not a dump from V8.

Running in
  1. script
Next: line 2
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
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(" | "));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Maglev: a fast SSA compiler

V8

V8'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.

A tiny Maglev-style planning modelPop out in the code editor (opens in a new tab)JavaScript
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.

Where the optimizing compilers fit
CompilerRoleHow to think about it
MaglevFast SSA compilerBuilds a compact CFG-shaped SSA graph from bytecode and feedback, then emits good-enough optimized code quickly.
TurboFanTop-tier optimizerSpends more compile time for peak V8 performance; its JavaScript backend now uses Turboshaft for the backend path.
TurboshaftCFG-shaped backend IRA newer V8 IR that replaces much of TurboFan's old Sea-of-Nodes backend with a more traditional control-flow graph shape.
WarpMonkeySpiderMonkey optimizerUses bytecode plus inline-cache data, including CacheIR snapshots, to build MIR and optimized code.
FTL with B3JavaScriptCore top tierCombines JSC's high-level optimizer with the B3 low-level backend for hot code.
Verified runtime fact

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

GRAPHS

TurboFan 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.

CFG blocks or Sea-of-Nodes edges?
Tiny program represented two waysPop out in the code editor (opens in a new tab)JavaScript
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));
Control-flow graphblocks
  1. Block entry: compute left, compute right, branch on flag
  2. Block then: picked is left
  3. Block else: picked is right
  4. 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.

Try it yourself

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.

Toggle the representation of the same tiny program. The JavaScript output is unchanged; only the compiler's internal view changes.

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.

Sea of Nodes and CFG are easy to confuse
RepresentationCore shapeWhy a compiler uses it
Control-flow graphBasic blocks plus edges for branches and jumps.Excellent when compiler passes want an explicit execution order.
Sea of NodesNodes mostly connected by value, effect, and control dependencies.Excellent when passes want pure operations to float until scheduling.
Turboshaft directionV8's JavaScript backend moved toward a CFG-shaped IR.Keeps many optimization benefits while improving compiler engineering and cache behavior.
The tiny program behind the graph playgroundPop out in the code editor (opens in a new tab)JavaScript
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));
Primary-source trail

WarpMonkey: CacheIR to MIR and trial inlining

SPIDERMONKEY

SpiderMonkey'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.

A tiny CacheIR-to-MIR teaching modelPop out in the code editor (opens in a new tab)JavaScript
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.

Source note

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

JSC

JavaScriptCore 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.

JavaScriptCore tier names in orderPop out in the code editor (opens in a new tab)JavaScript
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.

Three engines, three optimizer pipelines
EngineMid optimizerTop/back-end pathImportant caution
V8MaglevTurboFan with Turboshaft backend workIgnition and Sparkplug feed feedback and bytecode; Node 22's V8 12.4 build keeps Maglev off by default.
SpiderMonkeyWarpMonkeyMIR lowered to LIRCacheIR stubs from Baseline tiers are snapshotted and transpiled to MIR; trial inlining uses observed calls.
JavaScriptCoreDFGFTL using B3DFG performs high-level optimizations; FTL uses B3 as a low-level optimizing backend.
WebKit source trail

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

THREADS

Concurrent 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.

Real-life analogyA cook plans while lunch is served

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.

Concurrent compilation: the line keeps serving
Step 0 of 8Ready
Your turn: follow the blue line

Replay a conceptual concurrent-compilation timeline. It models thread roles; it does not inspect your browser's engine.

Running in
  1. script
Next: line 13
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
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;} 
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Concurrent compilation separates work by responsibility
PartJobWhy it matters
Main threadKeeps running baseline or older optimized code.Responsiveness matters more than blocking for a perfect compile.
Background compilerBuilds the optimized graph and machine code.It must use snapshots and guards because the program keeps changing.
Install pointSwitches to optimized code when ready and safe.The JavaScript result must be identical before and after the switch.
Node trace probe for concurrent TurboFan compilationJavaScript
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);
Stable V8 evidence asserted by the lesson testsText
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.
V8 options evidence checked by testsJavaScript
// 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.
Sort the optimizer vocabulary
  • 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.
Try it yourself
0 of 5 correct

Put each card under the kind of idea it describes.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

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.

Terms that sound similar but mean different things
TermMeansDoes not mean
IRA compiler's internal program representation.A JavaScript feature you can observe directly in normal code.
SSAA naming discipline where each value name is assigned once.A promise that JavaScript variables cannot change.
Phi nodeA merge value at a control-flow join.A runtime function JavaScript calls.
Concurrent compilationCompilation work can happen off the main thread.Your function runs in two different threads at once.

Practice exercises

6 EXERCISES
Exercise 1 · Warm-upPredict the branch output

What number prints for the member path?

Starter codePop out in the code editor (opens in a new tab)JavaScript
function finalPrice(isMember) {
  let discount = 5;
  if (isMember) discount = 20;
  return 100 - discount;
}
console.log(finalPrice(true));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeName the merge idea

    Which SSA idea merges the member and regular discount values after the branch?

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeChoose the graph shape

      Which representation groups instructions into basic blocks?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFind the Warp pipeline word

        Warp transpiles successful CacheIR snapshots into which middle representation?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · PracticeFix the timeline bug

          The starter timeline installs optimized code too early. Describe the correct ordering.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const timeline = [
            "main: install optimized code",
            "background: compile optimized graph"
          ];
          console.log(timeline.join(" -> "));

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeApply it to a real app

            A checkout page feels slow after a refactor. What should you do before rewriting code for Maglev, TurboFan, Warp, or FTL?

            Answer, then press Check. Spacing and letter case don’t matter.

              Quiz: check your understanding

              8 QUESTIONS

              Use the IR vocabulary, engine pipeline names, and thread model from the lesson.

              Lesson quiz · 8 questionsScore: first tries count
              1. Question 1 of 8Why do optimizing compilers build an IR before machine code?

                Choose an answer to see the explanation.

              2. Question 2 of 8What does this branch example print first?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                function finalPrice(isMember) {
                  let discount = 5;
                  if (isMember) discount = 20;
                  return 100 - discount;
                }
                console.log(finalPrice(true));

                Choose an answer to see the explanation.

              3. Question 3 of 8In SSA, what does a phi node do?

                Choose an answer to see the explanation.

              4. Question 4 of 8Which statement matches Maglev as described by V8?

                Choose an answer to see the explanation.

              5. Question 5 of 8What does this timeline model print last?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const 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.

              6. Question 6 of 8How does V8's 2025 Turboshaft direction relate to Sea of Nodes?

                Choose an answer to see the explanation.

              7. Question 7 of 8What does SpiderMonkey's Warp pipeline use CacheIR for?

                Choose an answer to see the explanation.

              8. 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.

              CompleteFrontend Clear concepts. Working examples.