V8pedia

Overview: the tier ladder

V8 does not have a compiler. It has an interpreter and three JIT compilers, arranged as a ladder, and a manager that decides when to move each function up or down. This page explains why that design exists — it is the central answer to "how does a dynamic language run fast?" — and links to the deep dive on each rung.

The fundamental tension

Optimizing compilation is a bet. You spend time and memory compiling fast code, betting the function runs often enough to pay it back. For a function called once, that bet loses badly. For a hot inner loop, it wins enormously. Since V8 cannot know in advance which is which, it does not guess: it starts cheap, measures, and invests progressively as evidence accumulates.

That is the whole idea behind tiering.

The four tiers

Tier Kind Compiles Runs Speculates?
Ignition bytecode interpreter instantly slowest no (it gathers feedback)
Sparkplug baseline JIT very fast (1:1) fast no
Maglev mid-tier optimizer fast faster yes (lightly)
TurboFan top-tier optimizer slow fastest yes (aggressively)

Each tier is a different point on the compile-time vs run-time curve. Ignition pays nothing to "compile" but runs slowly; TurboFan runs fantastically but costs a lot to produce. The intermediate tiers exist because the gap between "instant but slow" and "slow but fast" was too wide — a function could be stuck paying interpreter overhead for a long time while waiting for TurboFan, or never get hot enough to justify TurboFan yet still be worth some compilation.

::: info Why a "baseline" tier (Sparkplug) at all? Sparkplug does no optimization — it translates bytecode to machine code almost one instruction at a time. So why bother? Because it removes the interpreter's dispatch overhead (the indirect jump per bytecode) for nearly free compile cost, and it keeps the exact same stack frame layout as Ignition, so switching in and out is trivial. It is the cheapest possible speedup. See Sparkplug. :::

Who decides: the tiering manager

The decision to climb is driven by an interrupt budget: a counter, proportional to the function's bytecode length, that ticks down as the function runs. When it hits zero, the tiering manager wakes up and decides whether to compile, and to which tier.

DEFINE_INT(invocation_count_for_feedback_allocation, 8, …)  // → Sparkplug-ish
DEFINE_INT(invocation_count_for_maglev, 400, …)             // → Maglev
DEFINE_INT(invocation_count_for_turbofan, 3000, …)          // → TurboFan
DEFINE_INT(invocation_count_for_osr, 500, …)                // → OSR

src/flags/flag-definitions.h#L1143-L1169 (the Maglev default is 400 on non-Android, 1000 on Android)

::: warning These numbers are heuristics The thresholds above are tuning knobs, not contracts. They differ by platform, change between releases, and are overridden wholesale by modes like --jit-fuzzing or efficiency/battery-saver mode. Cite them to understand the mechanism; never depend on a specific value. :::

The budget is scaled by bytecode length, so a big function needs proportionally more invocations before V8 invests in optimizing it — small hot functions get optimized eagerly; large rarely-hot ones do not waste compile time. The full logic lives in Feedback & tiering decisions.

Climbing without restarting: OSR

A function stuck in a long-running loop would, naively, have to return and be called again before its optimized code took effect — useless for a loop that never returns. On-stack replacement (OSR) solves this by swapping the running frame for optimized code mid-loop.

Falling back down: deoptimization

The optimizing tiers speculate on types observed so far. When a speculation is violated — a function that always saw Smis suddenly sees a string — the code deoptimizes: V8 reconstructs the interpreter frame from the optimized one and resumes execution in a lower tier. Speculation + deopt is what makes aggressive optimization safe in a language where types can change at any moment.

The feedback that powers all of it

None of this works without data. As Ignition runs, inline caches record the shapes, call targets, and operand types each operation sees into a FeedbackVector. The optimizing compilers read that vector to decide what to specialize on. The interpreter is not just slow execution — it is the profiling phase. That is why V8 always starts in Ignition, even for code it will surely optimize.

Follow the ladder top to bottom: IgnitionSparkplugMaglevTurboFan, then the cross-cutting mechanisms: Feedback & tiering, Deoptimization, OSR.