Tiered compilation
Learn how JavaScript engines start with interpreters, watch hot code, then tier up through baseline and optimizing JIT compilers.
- 01Explain the compile-speed curveCompare interpreters, ahead-of-time compilers, and JIT compilers by startup cost, peak speed, and when each trade-off pays back.
- 02Name the major browser tiersPlace V8's Ignition, Sparkplug, Maglev, and TurboFan beside SpiderMonkey's and JavaScriptCore's current tier names.
- 03Recognize warm-up in real workUse hotness counters, tier-up, background compilation, benchmark warm-up, and Node trace output without writing code for a specific tier.
Start simple, then specialize
JavaScript engines do not know which code deserves expensive machine-code compilation when a page first loads. A tiny analytics helper might run once, while a parser, animation step, or hot render helper might run thousands of times. Tiered compilation is the engine's compromise: start cheap, watch what happens, then spend more compiler effort only where the program proves it is worth it.
Tiered compilation is a runtime strategy where an engine runs cold JavaScript in a quick tier, collects hotness and feedback, then recompiles frequently executed code into faster tiers with higher compile cost.
Keep the scope sharp. This lesson covers interpreters versus compilers, JIT compilation, the tier names in V8, SpiderMonkey, and JavaScriptCore, and the counters that trigger tier-up. The next lessons go deeper on type feedback, inline caches, speculative optimization, deoptimization, and inlining. For the larger map, revisit Meet the engines, Bytecode & interpreters, and Startup performance.
Interpreter, compiler, and JIT trade-offs
TRADE-OFFAn interpreter starts almost instantly because it can execute bytecode one instruction at a time. That makes it perfect for cold code, but each operation pays dispatch overhead. An ahead-of-time compiler spends time before execution to create fast machine code; that can be excellent for known programs, but it is too slow to do for every function on page load. A just-in-time compiler runs in the middle: compile at runtime when the engine has evidence that the compile cost will pay back.
A school can start with a rough timetable, then improve the plan for the classes that stay busiest. The first plan starts quickly; later improvements repay their work where they help most.
- In real life: A rough timetable works on the first day
- In JavaScript: Ignition or another interpreter can start cold code quickly
- In real life: A better timetable takes time to prepare
- In JavaScript: Optimized machine code takes compiler time up front
- In real life: Busy classes get the careful improvements
- In JavaScript: Only hot functions deserve expensive optimizing tiers
- In real life: A quick update helps before the final plan
- In JavaScript: A baseline compiler gives faster code without full optimization
Where the analogy stops: A school chooses its timetable deliberately. Engines use counters and feedback while preserving JavaScript semantics.
function totalTime(compileCost, callCost, calls) { return compileCost + callCost * calls;} console.log(totalTime(0, 5, 4)); // interpreter: cheap startconsole.log(totalTime(120, 0.6, 4)); // top tier: too much compile costconsole.log(totalTime(120, 0.6, 400)); // top tier: compile cost pays backLine 1 models the total cost as compile cost plus call cost. Line 5 is cheap to start: four interpreted calls cost 20 simulated units. Line 6 shows why top-tier code is a bad deal for a short script: the compile cost dominates. Line 7 shows the other side of the curve: hundreds of calls can repay expensive compilation.
| Tier | Compile cost | Run speed | Best fit |
|---|---|---|---|
| Interpreter | Almost no compile delay | Slowest per operation | Cold code, startup, code that runs once |
| Baseline JIT | Small compile cost | Faster dispatch and simple machine code | Functions that ran enough to justify quick native code |
| Mid-tier optimizing JIT | Moderate compile cost | Good code quickly | Hot code where top-tier compile time may still be too expensive |
| Top-tier optimizing JIT | Highest compile cost | Best peak speed after feedback | Very hot loops and long-lived functions |
V8: Ignition, Sparkplug, Maglev, TurboFan
V8V8's current teaching map is Ignition to Sparkplug to Maglev to TurboFan. Ignition is V8's bytecode interpreter. Sparkplug, announced by V8 in 2021, is a non-optimizing baseline compiler that compiles from bytecode directly to machine code. It is designed to be fast enough that short-lived code can benefit before a heavy optimizer would finish.
Maglev, introduced in Chrome M117 in 2023, sits between Sparkplug and TurboFan. V8 describes it as a fast optimizing JIT that uses feedback to make good-enough code quickly. TurboFan remains V8's top-tier optimizing compiler. Historically TurboFan used a Sea of Nodes intermediate representation; V8's 2025 “Land ahoy: leaving the Sea of Nodes” post describes the ongoing move toward the Turboshaft backend, a more control-flow-graph-shaped pipeline for the backend side.
Chrome and Node can expose different V8 defaults. The Node 22.23.1 build used in this lesson reports Sparkplug and TurboFan enabled by default, but its --v8-options output reports --maglev as default --no-maglev and the Maglev native probe says “Maglev is not enabled.” The browser pipeline still includes Maglev in Chrome releases.
- A function is parsed to bytecode and runs for the first time in V8.
- V8 translates bytecode to simple machine code without heavy optimization.
- A hot function needs better code quickly, but not the full top-tier compile bill yet.
- A very hot function is worth the highest compile effort for peak speed.
- JavaScriptCore starts with a low-level interpreter before JIT tiers.
Sort each card by the tier role it describes. The labels are conceptual; exact thresholds are engine choices.
SpiderMonkey and JavaScriptCore use the same idea
OTHER ENGINESTiered compilation is not a V8-only idea. Mozilla's SpiderMonkey source docs describe a tiering engine that increments warmup counters when entering functions and reaching loop headers. Its documented tiers are the C++ interpreter, Baseline Interpreter, Baseline Compiler, and Ion optimizing compiler. The 2020 Warp project changed how Ion gets optimization data: WarpBuilder snapshots bytecode and inline-cache data, then a background thread builds MIR through the Warp pipeline.
JavaScriptCore uses LLInt, Baseline JIT, DFG, and FTL. WebKit's 2016 B3 post explains that FTL switched from LLVM to the B3 backend, and WebKit's 2020 speculation post explains how JavaScriptCore uses speculation to make dynamic JavaScript run quickly. Details differ, but the curve is familiar: cheap start, quick baseline, stronger optimizing tiers.
| Engine | Start | Baseline | Middle | Top tier |
|---|---|---|---|---|
| V8 | Ignition | Sparkplug | Maglev | TurboFan with the Turboshaft backend replacing the old Sea-of-Nodes backend path in current work |
| SpiderMonkey | C++ interpreter, then Baseline Interpreter | Baseline Compiler | None with the Maglev name | Ion optimizing compiler built through the Warp pipeline from CacheIR and MIR/LIR |
| JavaScriptCore | LLInt | Baseline JIT | DFG JIT | FTL JIT using the B3 backend |
Hotness counters and tier-up
COUNTERSEngines need a signal that code is hot. A simple teaching model is an invocation counter: each function call increments a count, and thresholds request stronger tiers. Real engines combine invocation counts, loop back-edge or interrupt-budget counters, feedback vectors, inline-cache states, and heuristics. Optimizing compilation can happen on background threads so the main thread keeps running older code until the new code is ready.
const thresholds = { baseline: 6, mid: 30, top: 90 };let tier = "interpreter";let invocationCount = 0; function scoreCart(items) { invocationCount += 1; if (invocationCount === thresholds.baseline) tier = "baseline"; if (invocationCount === thresholds.mid) tier = "mid"; if (invocationCount === thresholds.top) tier = "top"; return { tier, total: items.reduce((sum, item) => sum + item.price, 0) };} for (let call = 1; call <= 8; call += 1) { const report = scoreCart([{ price: call }, { price: 2 }]); console.log(call, report.tier, report.total);}The model starts in interpreter, increments invocationCount on every call, and switches to baseline on call six. Later thresholds stand in for mid-tier and top-tier compilers. The important point is not the number six; it is that tier-up is driven by evidence from running code.
Replay a real simulator that increments a hotness counter, then tiers a hot function from interpretation to baseline code.
script
const thresholds = { baseline: 6, mid: 30, top: 90 };let invocationCount = 0; function scoreCart(items) { invocationCount += 1; if (invocationCount === thresholds.baseline) tier = "baseline"; if (invocationCount === thresholds.mid) tier = "mid"; if (invocationCount === thresholds.top) tier = "top"; return { tier, total: items.reduce((sum, item) => sum + item.price, 0) };} for (let call = 1; call <= 8; call += 1) { const report = scoreCart([{ price: call }, { price: 2 }]); console.log(call, report.tier, report.total);}let tier = "interpreter";let interruptBudget = 18; function crunchLoop(limit) { let total = 0; for (let i = 1; i <= limit; i += 1) { interruptBudget -= 3; if (interruptBudget <= 0 && tier === "interpreter") { tier = "baseline"; // a real engine can OSR into compiled loop code interruptBudget = 18; } total += i; } return { tier, total };} console.log(crunchLoop(8));Function calls are not enough. A single call can contain a long-running loop, so engines also check loop progress. On-stack replacement (OSR) lets a running loop switch into compiled code without starting the function over. This lesson shows the idea; a later optimization lesson covers the stack-frame mechanics.
Replay loop hotness with an interrupt budget. This is a teaching model for tier-up and OSR, not a debugger view of a real engine.
script
let interruptBudget = 18; function crunchLoop(limit) { let total = 0; for (let i = 1; i <= limit; i += 1) { interruptBudget -= 3; if (interruptBudget <= 0 && tier === "interpreter") { tier = "baseline"; // a real engine can OSR into compiled loop code interruptBudget = 18; } total += i; } return { tier, total };} console.log(crunchLoop(8));Compare never optimize, eager optimize, and tiered JIT
PLAYGROUNDThe same compile-time curve looks different for a short script and a long session. Never optimizing wins when nothing runs enough to pay back compilation. Eager optimization wastes time compiling cold helpers. Tiering wins the common browser case: start quickly, then spend compiler work on the function that became hot.
const costs = { interpreter: { compile: 0, call: 5 }, baseline: { compile: 15, call: 2 }, mid: { compile: 40, call: 1.2 }, top: { compile: 120, call: 0.6 },}; const program = [ { name: "route", calls: 3 }, { name: "format", calls: 160 }, { name: "validate", calls: 3 }, { name: "paint", calls: 3 }, { name: "cleanup", calls: 3 },]; // A real engine uses richer counters, feedback, and background compilation.// This model only shows the compile-time vs run-time trade-off.console.log("never optimize", 860);console.log("optimize eagerly", 703.2);console.log("tiered JIT", 422.6);860.0compile 0.0 · run 860.0 · top-tier functions 0
- interpret every call: 860.0
703.2compile 600.0 · run 103.2 · top-tier functions 5
- compile every function: 600.0
- run all calls: 703.2
422.6compile 175.0 · run 247.6 · top-tier functions 1
- format -> baseline: 55.0
- format -> mid: 143.0
- format -> top: 335.0
Tiered JIT is fastest for this long program because it pays 175.0 simulated compile units and 247.6 run units. The model is conceptual: real engines use feedback, OSR, background compilers, and different thresholds.
The model charges top-tier compile cost for every function in the eager strategy, while the tiered strategy only compiles functions that cross thresholds. Real engines use many more details, but this is the heart of tiered compilation: optimize what evidence says is worth it.
Real Node 22.23.1 and V8 12.4 evidence
EXPERIMENTV8 exposes internal testing functions when Node runs with --allow-natives-syntax. They are not JavaScript language features, and a normal browser or Node process treats the % syntax as invalid. The lesson tests run child Node processes, remove NODE_TEST_* variables, and assert only stable facts: version strings, V8 option defaults, status bits from V8 12.4's runtime.h, and --trace-opt lines that mention TurboFan compilation.
// Node 22.23.1 (V8 12.4). Run only in Node with --allow-natives-syntax.function hotAdd(x) { let total = 0; for (let i = 0; i < 30; i += 1) total += (x + i) | 0; return total;} %PrepareFunctionForOptimization(hotAdd);hotAdd(1);hotAdd(2);%OptimizeFunctionOnNextCall(hotAdd);hotAdd(3);console.log(%GetOptimizationStatus(hotAdd));In V8 12.4, the status bits include kIsFunction = 1 << 0, kOptimized = 1 << 4, and kTurboFanned = 1 << 6. The forced optimization probe is deterministic enough for a lesson test, but it is still an engine-lab tool. Do not ship code that calls native syntax or relies on a particular tier.
node --allow-natives-syntax --trace-opt tier-up-probe.mjsNode 22.23.1 (V8 12.4)
[manually marking ... hotAdd ... for optimization to TURBOFAN]
[compiling method ... hotAdd ... (target TURBOFAN)]
[completed compiling ... hotAdd ... (target TURBOFAN)]
status bits include kIsFunction, kOptimized, and kTurboFannedThe local Node 22.23.1 process reports V8 12.4.254.21-node.56. Its options list Sparkplug and TurboFan enabled by default, Maglev available as a flag but defaulting to --no-maglev, and Maglev background-code flags present. That is why the real forced optimization evidence in this lesson uses TurboFan, while the V8 pipeline section still teaches Maglev as a Chrome tier.
Use tiering knowledge safely
PRACTICALTiered compilation explains why benchmark warm-up matters and why short scripts often never reach a top tier. It does not mean you should write for Sparkplug, Maglev, TurboFan, Ion, or FTL directly. Engine teams change thresholds, tier names, and heuristics. Your job is to write clear code, measure the real interaction, warm up benchmarks, and understand why the first few runs may differ from the steady state.
| Situation | Tiering lesson | What to do |
|---|---|---|
| Benchmark warm-up | Run setup and warm-up before timing, then report several samples or a median. | See Measuring performance before trusting a microbenchmark. |
| Short scripts | A CLI startup path or one-off page script may never reach TurboFan or another top tier. | Optimize startup and parse cost before chasing peak JIT speed. |
| Hot paths | A frequently called render helper or parser can climb tiers. | Measure first, then simplify shapes and inputs when later feedback lessons teach why. |
| Production advice | Do not depend on % natives, tier flags, or one engine's thresholds. | Use portable JavaScript and benchmark the user-visible path. |
If a benchmark changes after warm-up, connect that behavior to Measuring performance. If a hot function behaves differently after its inputs change, the next lessons on type feedback and inline caches explain the evidence optimizers consume.
Common misconceptions
“JIT means everything is compiled immediately.”
No. Tiered JITs deliberately avoid expensive compilation for cold code.
“Interpreters are obsolete because compilers are faster.”
Interpreters are a startup optimization. Running soon can be more valuable than running peak-fast later.
“A function in TurboFan will always stay there.”
Optimized code depends on assumptions. Later lessons cover deoptimization when assumptions fail.
“All engines use V8's tier names.”
SpiderMonkey and JavaScriptCore have their own names, thresholds, and implementation details.
“Native syntax is a production performance API.”
It is a V8 testing interface behind flags. Production code should measure behavior, not force tiers.
| Idea | Means | Does not mean |
|---|---|---|
| JIT | Runtime compilation guided by evidence. | A promise that every function compiles immediately. |
| Interpreter | A fast-start execution tier for cold code. | A useless fallback that modern engines avoid. |
| Optimized code | Machine code guarded by assumptions and feedback. | Permanent code that can never deoptimize. |
| V8 native syntax | A flag-gated testing interface for experiments. | Portable production JavaScript. |
Practice exercises
5 EXERCISESWhat number prints for the four-call interpreter case?
function totalTime(compileCost, callCost, calls) {
return compileCost + callCost * calls;
}
console.log(totalTime(0, 5, 4));console.log(20);The interpreter case costs 0 + 5 × 4, so it prints 20.
At which call does the lesson simulator first switch from interpreter to baseline?
const thresholds = { baseline: 6, mid: 30, top: 90 };
console.log(thresholds.baseline);The simulator first compiles baseline code at call 6. Real engines choose their own thresholds.
Which V8 status bit name identifies top-tier TurboFan code?
The status mask includes kTurboFanned, proving the forced optimization reached V8's top-tier TurboFan code in this Node build.
What abbreviation names the mechanism that can switch a long-running loop to compiled code mid-execution?
OSR means on-stack replacement. It lets a running loop enter compiled code without restarting the whole function.
Should production application code call V8 native optimization functions?
// Should app code call %OptimizeFunctionOnNextCall in production?No. Use native syntax only for local engine experiments or tests. Production code should be portable and measured with user-facing metrics.
Quiz: check your understanding
8 QUESTIONSAnswer with the compile-time curve, tier names, and hotness counters in mind.
Question 1 of 8Why do modern JavaScript engines use tiered compilation?
Choose an answer to see the explanation.
Question 2 of 8What does this trade-off snippet print first?
Read the code, then predictfunction totalTime(compileCost, callCost, calls) { return compileCost + callCost * calls; } console.log(totalTime(0, 5, 4));Choose an answer to see the explanation.
Question 3 of 8Which V8 tier is the non-optimizing baseline compiler introduced in 2021?
Choose an answer to see the explanation.
Question 4 of 8Which statement best describes Maglev in V8's current pipeline?
Choose an answer to see the explanation.
Question 5 of 8What do SpiderMonkey warmup counters check?
Choose an answer to see the explanation.
Question 6 of 8What does this loop-budget model print?
Read the code, then predictlet tier = "interpreter"; let budget = 6; for (let i = 0; i < 3; i += 1) { budget -= 3; if (budget <= 0) tier = "baseline"; } console.log(tier);Choose an answer to see the explanation.
Question 7 of 8Why should production code avoid V8 native syntax such as
%GetOptimizationStatus?Choose an answer to see the explanation.
Question 8 of 8Which practical benchmark habit follows from tiered compilation?
Choose an answer to see the explanation.
Key takeaways
- Interpreters start quickly; compilers can run faster after paying compile time; JITs choose at runtime what is worth compiling.
- V8's modern map is Ignition, Sparkplug, Maglev, and TurboFan, with Turboshaft work replacing the old Sea-of-Nodes backend path.
- SpiderMonkey and JavaScriptCore use different tier names but the same economic idea: spend more compiler effort on hotter code.
- Hotness comes from function entries, loop progress, feedback, and thresholds; OSR lets loops switch tiers while already running.
- Warm up benchmarks, expect short scripts to stay in cheap tiers, and never write production code for a specific engine tier.
Remember the one-liner.
Tiered compilation is the engine's pay-as-you-go plan: start cheap, watch real execution, and spend compiler effort only where it pays back.
Up next: Type feedback & feedback vectors.