V8 Engine · Lesson 2

The Architecture Map

Why this lesson: Your mission is to draw V8's pipeline and explain when and why each tier kicks in. Lesson 1 showed you station one — bytecode. Today you'll see the whole assembly line move: you'll watch a function climb from bytecode to optimized machine code, and then watch it fall back down when V8's bet goes wrong.

Here's the single most important idea in V8's design, and the key to the whole mission:

V8 is a speculative engine. It doesn't know your types ahead of time (it's JavaScript). So it runs your code cheaply at first, watches what types actually show up (that's the feedback vector from Lesson 1), and once a function is hot, it bets those types will keep showing up and compiles fast machine code specialized to them. If the bet later breaks, it throws the machine code away and drops back to the interpreter.

Everything below is a consequence of that one idea. Let's map the tiers, then watch them in action.

The tiers: cheap-to-make vs fast-to-run

V8 has one interpreter and three compilers. They trade off the same two things in opposite directions: how expensive the code is to produce, and how fast it runs. A function moves up this ladder only as it proves it's worth the investment.

← cheap to produce, slow to runexpensive to produce, fast to run →
Parser→ AST
Source text → Abstract Syntax Tree. Not execution — just structure. (Lazily; functions are often pre-parsed and only fully parsed when first called.)
Ignitioninterpreter
Generates bytecode and runs it. Tiny footprint, instant startup. Most code lives and dies here. Also records type feedback as it runs.
Sparkplugbaseline JIT
Compiles bytecode straight to machine code with no optimization — a near-mechanical 1:1 translation. Very fast to produce; removes interpreter overhead. A quick win for warm code.
Maglevmid-tier JIT
First optimizing compiler. Uses the recorded type feedback to specialize. Good optimizations, moderate compile cost. The middle ground.
TurboFantop-tier JIT
The most aggressive optimizer. Expensive to run, produces the fastest code. Reserved for the hottest functions. Compiles concurrently on a background thread.

Sources: Ignition & TurboFan · Sparkplug · Maglev. The exact tiers a function visits depend on heuristics and V8 version — not every function stops at every rung.

Why so many tiers? Because optimizing has a cost. If V8 fully optimized every function on first sight, startup would crawl and it would waste effort on code that runs once. The tiers let V8 spend optimization budget only where it pays off — hot code — while everything else starts instantly as bytecode.

Watch it: a function climbing the tiers hands-on

1A hot function. Call something enough times that V8 decides it's worth optimizing:

cat > /tmp/tiers.js <<'EOF'
function square(x) { return x * x; }
let total = 0;
for (let i = 0; i < 200000; i++) { total += square(i); }
console.log("done", total);
EOF
node --trace-opt /tmp/tiers.js

Filter the noise to just our function and you'll see the climb:

[marking ... square ... for optimization to MAGLEV, ... reason: hot and stable]
[compiling method ... square ... (target MAGLEV), mode: ...kConcurrent]
[completed compiling ... square ... (target MAGLEV) - took 0.0 ms]
[marking ... square ... for optimization to TURBOFAN_JS, ... reason: hot and stable]
[completed optimizing ... square ... (target TURBOFAN_JS)]

Read that as a story: V8 ran square as bytecode, noticed it was "hot and stable" (called a lot, always with numbers), promoted it to Maglev, then as it kept running, promoted it again to TurboFan — the top tier. Each kConcurrent means the compile happened on a background thread while your code kept running.

You may also spot OSR. That's On-Stack Replacement: the loop itself is still running when the optimized code becomes ready, so V8 swaps the running function's machine code mid-flight — replacing it on the stack without waiting for the loop to finish. It's how a long loop benefits from optimization that finished after the loop started.

Watch it: the bet breaking (deoptimization) hands-on

Optimized code is fast because it assumes things — "this object always has property x in this exact layout." Break the assumption and V8 must bail out. Let's force it:

2Optimize for one shape, then change the shape:

cat > /tmp/deopt.js <<'EOF'
function load(o) { return o.x; }
let a = { x: 1 };
for (let i = 0; i < 200000; i++) { load(a); }   // optimize assuming shape {x}
let b = { y: 9, x: 2 };                       // different shape!
load(b); load(b);
EOF
node --trace-opt --trace-deopt /tmp/deopt.js

Among the tier-up lines, the payoff:

[bailout (kind: deopt-eager, reason: wrong map): begin.
   deoptimizing ... load ..., <Code MAGLEV> ...]

There it is: "reason: wrong map". A Map is V8's internal name for an object's hidden class — its shape. The optimized load was compiled assuming objects shaped like {x}. The moment it met {y, x}, the assumption was wrong, V8 deoptimized — threw away the machine code and resumed in the interpreter. (That hidden-class machinery is a whole lesson of its own, coming up.)

Term in the logWhat it's telling you
marking … for optimizationV8 decided this function is worth compiling to a higher tier.
reason: hot and stableWhy it tiered up: called often (hot), consistent types (stable).
ConcurrencyMode::kConcurrentThe compile ran on a background thread; your JS kept executing meanwhile.
OSROn-Stack Replacement — optimized code swapped into a currently-running loop.
bailout / deoptimizingAn optimization assumption broke; falling back to the interpreter (bytecode).
reason: wrong mapThe specific broken assumption: the object's shape (hidden class) wasn't what the code expected.

Check yourself feedback loop

1. Why does V8 have multiple tiers instead of just always producing the fastest machine code?
2. A function gets optimized to TurboFan, then deoptimizes with reason: wrong map. What happened?
3. What is the feedback vector (from Lesson 1) actually for in this bigger picture?
4. Order the journey: drag your memory, then check. Put these in the order a hot, stable function experiences them.
Ask me anything. Want to see a function get stuck at Sparkplug and never reach TurboFan? Curious what makes code "unstable" so it never optimizes? Want me to write a snippet that deopts over and over (a real perf bug pattern)? Just ask — e.g. "Show me a polymorphic function that won't optimize well," or "What's the difference between eager and lazy deopt?"

Where this lands you

You can now draw V8's full pipeline and narrate the dynamics: cheap bytecode first, feedback-driven tier-up for hot code, speculative machine code, and deopt when the speculation fails. That mental model is the spine everything else hangs on — including why certain JS patterns are fast (they stay "stable" and keep their optimized code) and others are slow (they deopt). We'll cash that out fully in the hidden-classes lesson.

Next: Lesson 3 — Build V8 from source. Time to stop borrowing V8 from Node and get your own copy compiling, so you can run d8 and, soon after, drive the engine from your own C++.

Reference docs: V8 Glossary (updated with this lesson's terms) · V8 Inspection Flags · Back to Lesson 1

V8 Engine learning track · Lesson 2 · Built around your mission: deep architectural understanding via the embedding API.