open atlas
↑ Back to track
JavaScript Engine internals JSE · 04 · 01

Four tiers and how warm-up works

V8 runs four execution tiers — Ignition, Sparkplug, Maglev, TurboFan — that trade compile cost for run speed. How invocation counts and loop back-edges drive tier-up, and why higher tiers compile concurrently on background threads.

JSE Middle ◷ 13 min
Level
FoundationsJuniorMiddleSenior

You run a function 10,000 times in a benchmark and watch its per-call time fall in three distinct steps — a small drop, then a bigger one, then a final cliff into native speed. Those steps are not noise. They are your function climbing four separate compilers, each willing to spend more time producing faster code than the one below it. Knowing which step you are on, and what triggers the next, is the difference between a benchmark that warms up and a production path that never does.

Four tiers, one cost-versus-speed curve

The overview lesson V8’s four-tier JIT pipeline sketched the ladder; this unit opens each rung. Start with the shape of the trade. Every tier sits at a point on a single axis: how long it takes to produce code versus how fast that code runs. Cheaper to compile means slower to run; faster to run means more expensive to compile. V8 ships four because no single point on that curve is right for all code.

Ignition is the interpreter. The parser hands it an AST; Ignition lowers it once to compact, register-based bytecode and then executes that bytecode on a virtual machine. There is no machine-code generation here at all — execution is a dispatch loop that reads a bytecode, jumps to a handler, runs it, and advances. Entry cost is effectively zero (you already paid to produce the bytecode), and it is the only tier that records the type feedback the upper tiers depend on. Per operation it is roughly an order of magnitude slower than optimised native code, because every step pays the interpreter’s table-lookup-and-indirect-jump dispatch overhead.

Sparkplug is the baseline JIT, shipped in V8 9.1 (2021). It is the rung people most often misunderstand, so be precise: Sparkplug does no optimisation. It builds no intermediate representation (IR), performs no inlining, no escape analysis, no type specialisation. It walks the existing bytecode once and emits a near-1:1 template of machine instructions — for each bytecode, a fixed small block of native code plus the occasional call back into the runtime. What it removes is exactly the interpreter’s dispatch overhead: no more table lookup and indirect jump per operation, just straight-line native code. That alone buys roughly 1.5–2× over Ignition, and it compiles in microseconds (about 1 ms per kilobyte of bytecode). Because compilation is so cheap, Sparkplug code can be produced lazily on first hotness or eagerly.

Maglev is the mid-tier optimising compiler, shipped in 2023. It is a genuine optimiser — it builds an SSA-form IR and uses the recorded type feedback to specialise property loads and arithmetic — but a lightweight one. It uses a fast linear-scan register allocator and skips TurboFan’s heaviest passes (escape analysis, polymorphic inlining, iterative type narrowing). The result compiles about 10× faster than TurboFan while reaching roughly 50–70% of its code quality: the right tool for “hot but not the hottest” functions, where TurboFan’s compile latency would hurt more than its extra speed helps.

TurboFan is the full optimising compiler — sea-of-nodes IR, aggressive inlining, escape analysis, the works (lesson 03 of this unit opens it). It produces the fastest code V8 can make, at a compile cost of tens to hundreds of milliseconds per function. You only want to pay that for code that is both very hot and type-stable.

How warm-up actually triggers a tier-up

A function does not climb because of a wall-clock timer. It climbs because two counters cross budgets. The first is the invocation count — how many times the function has been entered. The second is the back-edge count — how many times execution has jumped backwards to the top of a loop inside the function. V8 maintains a per-function budget (the “interrupt budget” / feedback budget); each invocation and each loop back-edge decrements it, and when it reaches zero the runtime fires a tier-up check. This is why both hot functions (called often) and hot loops (iterated often, even inside a function called once) can request a higher tier — the back-edge counter is what lets a single long-running loop get optimised without ever returning.

// Called many times -> invocation count drives tier-up.
function distance(a, b) {
  const dx = a.x - b.x, dy = a.y - b.y;
  return Math.sqrt(dx * dx + dy * dy);
}
for (let i = 0; i < 1_000_000; i++) distance(p, q);

// Called once, but the loop's back-edges drive tier-up of THIS frame.
function sumTo(n) {
  let total = 0;
  for (let i = 0; i < n; i++) total += i; // each iteration = one back-edge
  return total;
}
sumTo(100_000_000);

The thresholds are dynamic and tuned per release — roughly speaking, Sparkplug kicks in after low hundreds of invocations, Maglev after low thousands, TurboFan after tens of thousands — but never hardcode a number; treat them as ordering, not constants.

The tier ladder, by the numbers
Ignition compile cost
0 (already bytecode)
Sparkplug compile rate
~1 ms / kB bytecode
Sparkplug speedup over Ignition
~1.5-2x
Maglev compile time
~10 ms / function
Maglev code quality vs TurboFan
~50-70%
TurboFan compile time
tens-hundreds of ms
Sparkplug shipped
V8 9.1 (2021)
Maglev shipped
V8 11.x (2023)

Concurrent compilation: the interpreter never blocks

You might expect a 100 ms freeze when a function graduates to TurboFan — if compilation were synchronous, you would get exactly that. It is not. A tier-up does not stop your program. When the budget trips, V8 enqueues a compilation job and a background thread runs Maglev or TurboFan while the main thread keeps executing the function on its current tier. When the optimised code is ready, V8 swaps it in for the next entry (or mid-loop, via on-stack replacement — lesson 06). This is why you never see a 100 ms freeze when a function tiers up to TurboFan: the freeze would happen if compilation were synchronous, but it is not. The cost shows up instead as background CPU and a brief window where the function runs at the old speed despite being “scheduled” for the new tier.

You can watch all of this. --trace-opt logs every optimisation decision (which function, which tier, why); --no-opt disables the optimising tiers entirely so you can compare against pure Ignition plus Sparkplug. In Node: node --trace-opt --trace-deopt app.js. In d8, add --allow-natives-syntax to call %OptimizeFunctionOnNextCall(fn) and force a tier-up immediately rather than warming up by hand.

Quiz

What does Sparkplug actually do to your bytecode?

Quiz

A function is called exactly once but contains a 50-million-iteration loop. What lets V8 optimise it?

Order the steps

Order V8's tiers from cheapest-to-produce (runs first) to most heavily optimised (only the hottest code).

  1. 1 Ignition — interpret bytecode, record type feedback
  2. 2 Sparkplug — baseline JIT, 1:1 machine-code template
  3. 3 Maglev — mid-tier SSA optimiser, lightweight specialisation
  4. 4 TurboFan — full optimiser, fastest code
Why this works

Why add Sparkplug and Maglev between Ignition and TurboFan rather than just tuning thresholds? Because the gap was both wide and bimodal. Sparkplug closes the dispatch-overhead gap for almost free, so warm-but-not-hot code stops paying the interpreter tax during page load. Maglev closes the specialisation gap for medium-hot code without TurboFan’s compile latency, which matters for frame-time budgets — a 60 fps animation that called TurboFan on every newly-hot helper would blow its 16 ms frame on background compile contention. Two cheap intermediate rungs beat one big jump.

Recall before you leave
  1. 01
    Name V8's four tiers in order and the single defining property of each.
  2. 02
    What two runtime counters drive tier-up, and why are both needed?
  3. 03
    Why does a tier-up to TurboFan not freeze the main thread for 100 ms, and how would you observe the whole process?
Recap

V8 executes JavaScript across four tiers arranged on a single compile-cost-versus-run-speed curve. Ignition interprets register-based bytecode at zero compile cost and is the only tier that records the type feedback the optimisers need. Sparkplug is a baseline JIT that emits a near-1:1 machine-code template in microseconds, deleting interpreter dispatch overhead for a 1.5-2x win — but it performs no optimisation whatsoever: no IR, no inlining, no specialisation. Maglev is a lightweight SSA optimiser that uses feedback to specialise arithmetic and property access, compiling ~10x faster than TurboFan at ~50-70% of its code quality. TurboFan is the full optimiser, the fastest and most expensive. Functions climb the ladder when their invocation count and loop back-edge count trip a per-function budget — the back-edge counter is what lets a once-called function with a long loop get optimised. Compilation runs on background threads, so the main thread keeps running the current tier and never blocks; the new code is swapped in on the next entry. Watch it all with —trace-opt, —no-opt, and the natives-syntax intrinsics in d8. Now when you see three distinct speed steps in a benchmark, you know exactly which tier boundary each drop represents — and which counter to watch to make your production path cross it.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 5 done
Connected lessons
appears again in184

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

Apply this

Put this lesson to work on a real build.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.