Here's the single most important idea in V8's design, and the key to the whole mission:
Everything below is a consequence of that one idea. Let's map the tiers, then watch them in action.
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.
Sources: Ignition & TurboFan · Sparkplug · Maglev. The exact tiers a function visits depend on heuristics and V8 version — not every function stops at every rung.
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.
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.
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 log | What it's telling you |
|---|---|
| marking … for optimization | V8 decided this function is worth compiling to a higher tier. |
| reason: hot and stable | Why it tiered up: called often (hot), consistent types (stable). |
| ConcurrencyMode::kConcurrent | The compile ran on a background thread; your JS kept executing meanwhile. |
| OSR | On-Stack Replacement — optimized code swapped into a currently-running loop. |
| bailout / deoptimizing | An optimization assumption broke; falling back to the interpreter (bytecode). |
| reason: wrong map | The specific broken assumption: the object's shape (hidden class) wasn't what the code expected. |
reason: wrong map. What happened?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