Sparkplug and Maglev: Tiering Up

Switch off two of V8’s four compilers and every JavaScript program ever written still runs. For most of V8’s life that was not a thought experiment. The engine shipped with two gears: a slow interpreter to start instantly, and a heavy optimizing compiler for the hot paths. Between them, nothing.

That sounds fine until you meet the code that lives in the middle. Picture the function a table calls once per row while it renders five thousand rows:

// Called 5,000 times in one render. Busy enough that the
// interpreter's per-instruction tax adds up. Not obviously
// a "hot loop" you would hand to a heavy compiler.
function formatRow(row) {
  return row.name.padEnd(20) + " " + (row.amount / 100).toFixed(2);
}

Too busy to leave crawling in the interpreter. Not hot or stable enough to deserve the top optimizer, which is slow to produce code and might throw the result away the instant your data changes shape. Warm code fell into a gap and got the worst of both tiers.

This lesson is about how V8 filled that gap. The pipeline overview lined up four boxes and named them. Here we open the two in the middle, Sparkplug and Maglev, and the machinery that decides when a function has earned a climb.

The cliff between two speeds

Start with why two tiers is not enough, because that is the whole reason the other two exist.

The interpreter, Ignition, is cheap to start and slow to run. It pays a fetch-decode-dispatch cost on every single bytecode, so a loop body that spins a million times pays that tax a million times. The top optimizer sits at the other extreme. It produces the fastest code V8 can generate, and it is expensive: it does deep analysis, takes real time and memory, and only pays for itself when the code runs an enormous number of times. Hand it a function that runs a few thousand times and you spend more compiling than you ever save.

So a warm function has two bad options in a two-tier world. Stay in the interpreter and crawl. Or force a top-tier compile it does not deserve, stalling to produce code that might be invalidated moments later. There is no gentle in-between, no way to buy a little speed for a little cost.

two gears: a cliffIgnitioninstant, slowTurboFancostly, fastone giant leapwarm code stuck belowfour gears: a smooth rampIgnitionSparkplugMaglevTurboFan
Two tiers leave a gap that warm code falls into. Extra tiers turn one giant leap into a smooth ramp, a rung for every temperature.

Every tier is one point on the same two-axis tradeoff: how much it costs to produce the code, against how fast that code runs. Ignition is nearly free to produce and slow. The top tier is expensive and fast. The interesting question is what sits between them, and the answer is not a straight line. The cheap early tiers buy most of the speed for very little cost; the top tier pays a lot for the last stretch.

run speed →compile cost →Ignitioncheap, slowSparkplugbig win, tiny costMaglevreal optimization, fast to buildTurboFanpeak, priceybig cost,small extra gainmost of the speed,little of the cost
Each tier is a point trading compile cost for run speed. The curve bends early: cheap tiers capture most of the win, the top tier pays a lot for the final gain.

That bend is the argument for middle tiers in one picture. If a cheap compiler can capture a big chunk of the available speed, you want it, because most functions are never hot enough to justify paying full price. Sparkplug and Maglev are the two rungs that turn the cliff into a ramp.

Sparkplug: machine code, almost for free

Sparkplug is the first rung, a baseline compiler. Its job is narrow and it does it fast. It takes a function’s bytecode and produces machine code, and it does essentially no optimization along the way.

Here is the part that makes it cheap. Sparkplug does not build an intermediate representation, does not analyze types, does not allocate registers cleverly, does not reorder anything. It walks the bytecode once, top to bottom, and for each instruction emits a fixed lump of machine code that does exactly what the interpreter would have done for that instruction. The compiler is close to a big switch over opcodes, one case per bytecode, each case stamping out its canned machine-code stencil. One linear pass, no thinking.

bytecode (from Ignition)Ldar bAdd r0ReturnSparkplugone pass, a big switchmachine code, straight throughload b into a registeradd r0, check for numbersreturn the resultcompileemitno fetch-decode-dispatch loop; the CPU runs it directly
Sparkplug walks the bytecode once and stamps out a fixed machine-code stencil per instruction. No IR, no type analysis: the decode work is done once here instead of on every run.

So what did that buy? Look at what disappeared. The interpreter spends a chunk of its time on the loop itself: read the next opcode, work out which operation it is, jump to the handler, come back, repeat. Sparkplug’s output has none of that. Each operation is already spelled out as native instructions in a straight line, so the CPU just runs them. Same logic as the interpreter, minus the decode-and-dispatch overhead on every step. That single removal is a solid speedup, and because the compile is a one-pass stamping job, V8 can tier a function up to Sparkplug far more eagerly than it could ever justify a top-tier compile.

There is a second trick that matters more than it looks. Sparkplug keeps the function’s stack frame laid out exactly the way the interpreter lays it out: same slots, same locals, same arguments, in the same places.

When Sparkplug first shipped, the V8 team measured single-digit to low-double-digit percentage gains on real browsing workloads and benchmark suites. Those are launch-era, workload-specific numbers, so do not carry them around as a constant. The durable point is the shape of the deal: a large speedup over the interpreter, for a compile cost so low the engine barely has to think about whether a function deserves it.

Maglev: real optimization, in a hurry

Sparkplug removes overhead but never makes your code smarter. It still adds two numbers the long way, still checks types it could have proven, still reloads a property it just read. To go faster you need a compiler that actually reasons about the code. That is the top tier’s whole job, but the top tier is slow. Maglev is the compromise: a mid-tier optimizing compiler that does genuine optimization while staying cheap enough to run on far more functions.

Maglev is a real optimizer, and unlike Sparkplug it builds a proper intermediate representation: a control-flow graph in SSA form, the classic shape optimizing compilers work on. It reads the same type feedback the interpreter gathered (inline caches and type feedback are the next lessons) and uses it to specialize. If the feedback says a value has always been a number, Maglev can unbox it and do direct machine arithmetic instead of the generic, tagged, could-be-anything add. If a property access always saw the same object shape, Maglev can emit a shape check and a direct field load instead of a full lookup.

// Maglev specializes this from what Ignition recorded: v has
// always been { x, y } with number fields. So it checks the
// shape once, unboxes x and y, and emits direct float math
// instead of generic + and *.
function length(v) {
  return Math.sqrt(v.x * v.x + v.y * v.y);
}

What it deliberately does not do is the top tier’s heaviest work: aggressive inlining across many functions, the deepest redundancy elimination, the most expensive analyses. It picks the optimizations with the best payoff-per-millisecond and stops there. The result, in the V8 team’s own rough framing, is a compiler that runs about ten times slower than Sparkplug and about ten times faster than the top tier, producing code much better than Sparkplug’s and not far off the peak.

That middle slot earns its keep for a reason that is easy to miss: it is not only about the code that reaches Maglev. Before Maglev existed, a function hot enough to want real optimization had only the top tier to go to, and the top tier is slow to compile and runs its heaviest passes where they can cause main-thread jank and drain battery. Maglev lets a lot of that code get most of its speedup sooner and cheaper, and reserves the expensive top tier for the genuinely hottest paths that can pay it back.

TurboFan, in one paragraph

The top of the ladder is TurboFan, and it gets its own lesson next, so just the one-line placement here. It runs the full optimizing pipeline, produces the fastest code V8 can make, and is by far the most expensive tier to run, so only the hottest, most stable functions ever reach it. It compiles on a background thread so it does not stall your program, and every bet it makes is backed by deoptimization: if a speculative assumption breaks, the fast code is thrown out and execution falls back to the safe bytecode. The overview sketched all of this; the next lesson opens the box.

Hotness: how a function decides to climb

Nothing above happens on a schedule. A function is not promoted because time passed; it is promoted because it ran enough. V8 keeps counters. It tracks how many times a function has been called and how many times its loops have gone around, and it spends a kind of budget as the code runs. Each time the budget is exhausted, the engine takes that as a signal the function is worth more compile effort and tiers it up to the next rung.

That is why the same function is a different speed at different moments in its life. Cold, it interprets. A little use and it is running baseline machine code. Keep leaning on it and it climbs into the optimizers. It warms up.

one function, warming up over timeIgnition1xSparkplug~4xMaglev~10xTurboFan~20xpromotepromotepromote0how many times the function has run →relative, illustrativerun count climbing
One function, warming up. As its run count climbs and crosses V8's internal thresholds, it is promoted to faster tiers. The exact thresholds are engine-internal and change.

This warm-up is exactly why a benchmark’s first iterations lie about its later ones. Time a function once and you are mostly measuring the interpreter plus a compile. Time it in a tight loop and you may be measuring optimized code specialized on inputs that never occur in production. If you have ever seen the first few runs of a loop clock in slow and then the numbers drop off a cliff and stay down, you were watching the tiers climb underneath you. Benchmarking traps is a whole lesson on getting this right.

You can feel the warm-up directly below. Call the function a few times and it interprets. Keep calling and it crosses thresholds, promotes tier by tier, and speeds up, paying a one-time compile cost at each promotion. Everything here is a canned simulation with round, obviously-fake thresholds, not a real measurement:

interactiveWarm a function up through the tiers

Catching a loop mid-flight: on-stack replacement

There is a hole in “promote a hot function to the next tier”. Promotion normally takes effect the next time the function is called. So what happens when a function is called once and then sits in a loop that runs for a very long time?

// This call never returns until the loop finishes, so "optimize
// on the next call" would never fire. The engine has to upgrade
// the code while this exact invocation is still running.
let total = 0;
for (let i = 0; i < 1e9; i++) {
  total += i % 7;
}

The loop is blazingly hot, but the function around it is called exactly once. Waiting for a next call is useless; there may not be one for minutes. V8’s answer is on-stack replacement: it swaps the code for a function that is already running, in the middle of a loop, without waiting for it to return.

on-stack replacement: upgrading a loop mid-flightrun(): one invocation, still on the stackOSR: optimized code is ready, swap here at a back-edgeIgnition / Sparkplugslow, but already runningMaglev / TurboFanfast, swapped in mid-looploop iterations →back-edge: each iteration returns to the top
On-stack replacement upgrades a running loop. At a loop back-edge V8 switches the executing code from slow to fast while the same invocation stays on the stack. The frame does not leave; the code beneath it changes.

At a loop back-edge, V8 enters an optimized version of the code built to pick up right there at the loop header, carrying the current loop variables over, and execution keeps going at the faster tier from the next iteration. The invocation never left the stack. The code running underneath it got replaced.

This is where Sparkplug’s frame trick pays off again. Because a Sparkplug frame matches the interpreter’s, swapping a running loop up to baseline code needs almost no frame surgery. Jumping into an optimizing tier is heavier, since the optimized frame is shaped differently and V8 has to translate state into it, but it still happens at a clean loop boundary rather than anywhere in the middle. Either way, a long-running loop does not have to stay slow just because its function is only called once.

Every serious engine tiers

The names in this lesson are V8’s, and only V8’s. The idea is universal. Once you accept that a single interpreter-to-optimizer jump is too big, you end up building rungs, and every major engine did.

What this means for your code

You do not aim code at Sparkplug or Maglev, the same way you do not aim it at Ignition. There is no annotation that requests a tier, and chasing thresholds with tricks is a great way to write ugly code that a future engine makes pointless. A few things are worth actually keeping:

  • Warm-up is real, so measure the state you ship. The first runs of anything are cold. A benchmark that does not warm up is measuring a tier your users rarely hit, and a benchmark that loops forever may be measuring one they never hit either. Warm up, use representative inputs, and read benchmarking traps before you trust a number.
  • The tiers exist so you do not have to choose between fast-to-start and fast-to-run. Your code is responsive immediately because it interprets, and the parts that matter get faster on their own because they climb. That is the deal the middle tiers make possible.
  • You help the optimizers by being consistent, not clever. Maglev and TurboFan specialize on the types and shapes they observed. Code that does one steady thing with steady data gives them bets they can keep; wildly polymorphic hot paths keep them generic. That is the subject of the next lessons on type feedback, and you get there by writing plain, predictable functions.
  • Do not memorize the thresholds. They are internal, tuned every release, and vary by platform. “A little use gets the baseline, a lot of consistent use climbs into the optimizers” is the whole model you need.

Summary

  • V8 has four tiers, not two, because a single jump from a slow interpreter to a heavy optimizer leaves warm code with no good option. The middle tiers turn that cliff into a ramp.
  • Sparkplug is a baseline compiler: one linear pass over bytecode, no IR and no optimization, emitting a fixed machine-code stencil per instruction. It removes the interpreter’s decode-and-dispatch overhead for very little compile cost, and its interpreter-compatible stack frames make swapping in and out cheap.
  • Maglev is a mid-tier optimizer: it builds an SSA control-flow graph, specializes from type feedback (unboxing, shape checks), and lands around ten times slower than Sparkplug and ten times faster than the top tier. It captures most of the available speed without the top tier’s price.
  • TurboFan (next lesson) is the peak: the fastest code, the highest compile cost, reserved for the hottest stable functions, backed by deoptimization.
  • Code climbs by hotness: V8 counts calls and loop iterations and promotes a function when it crosses internal thresholds, so functions warm up over time. Those thresholds are engine-internal and change; the direction is the durable part.
  • On-stack replacement upgrades a loop that is already running, swapping its code at a back-edge so a long loop in a once-called function does not stay slow.
  • Tier names are V8-specific and evolving, but every serious engine tiers: a baseline plus one or more optimizing compilers, hotter code climbing higher.

What this means for your code: you never target a tier, but knowing your functions warm up explains why cold benchmarks lie, why a responsive start and a fast steady state can coexist, and why consistent code is what the optimizers reward.