V8 — Ignition & TurboFan
V8 runs your JavaScript in two tiers: Ignition (an interpreter that produces bytecode and collects type feedback) and TurboFan (an optimising compiler that uses that feedback to produce machine code). Predictable types win; surprises trigger deoptimisation.
Long read · the two-tier JIT, hidden classes, inline caches, and what makes code fast
1 · The pipeline
A function lifecycle in V8 looks like: parser → Ignition bytecode → optionally TurboFan machine code → optionally deoptimised back to bytecode. The interpreter runs everywhere initially; only hot functions get compiled.
function add(a, b) { return a + b; }
add(1, 2);
// Run with: node --print-bytecode file.js
// Roughly:
[generated bytecode for function: add]
0 Ldar a1 ; load 'b'
2 Add a0, [0] ; add to 'a' with feedback slot 0
5 ReturnThe [0] is a feedback vector slot. Each call records what types it saw —
Ignition uses that to decide whether to escalate to TurboFan, and TurboFan uses it to
decide which fast path to compile.
2 · Hidden classes
Internally, V8 doesn't store JavaScript objects as hash maps. It assigns each object a hidden class (also called a "map" or "shape") that describes its property layout. Objects with identical property names in the same insertion order share the same hidden class.
// These two objects share a hidden class — fast
const a = { x: 1, y: 2 };
const b = { x: 3, y: 4 };
// Different insertion order — DIFFERENT hidden class
const c = { y: 2, x: 1 };
// Adding a property later creates a transition to a new hidden class
const d = { x: 1 };
d.y = 2; // hidden class transition
d.z = 3; // another transition
// The "transition tree" branches; objects that share a path share a class.3 · Inline caches
At each property access, V8 maintains an inline cache (IC). The first time
obj.x runs, V8 records the hidden class of obj and the offset
where x lives. Subsequent accesses with the same hidden class skip the
lookup — just load from the cached offset.
| IC state | Shapes seen | Cost |
|---|---|---|
| uninitialised | 0 | full lookup |
| monomorphic | 1 | cached offset — fast |
| polymorphic | 2–4 | switch on shape — slower |
| megamorphic | 5+ | full lookup every time |
4 · TurboFan — the optimiser
After a function is "warm" (run enough times), V8 sends it to TurboFan. TurboFan reads the feedback vector and emits machine code specialised to the types it saw. Operations are unboxed where possible, properties are accessed at fixed offsets, and call sites are inlined.
The specialisation has a guard at the top: "the inputs are still the same shape we compiled for". If a guard fails (e.g. you pass a string into a function that always saw numbers), the function deoptimises — control transfers back to the bytecode and the optimised code is discarded.
5 · Reading what V8 thinks
# See bytecode
$ node --print-bytecode hot.js
# See optimisations
$ node --trace-opt --trace-deopt hot.js
# Output:
# [marking opt:0xdeadbeef for non-concurrent optimization]
# [compiling method:fn using TurboFan]
# [deoptimizing (DEOPT eager): begin opt_count=1]
# reason: wrong map
# See the optimised assembly (debug build only)
$ node --print-opt-code hot.js6 · What triggers deoptimisation
- Type mismatch. Function compiled for
(number, number), called with(string, number). - Hidden-class change. Object that was shape A is now shape B at the cached site.
- Out-of-bounds access. Array index outside the compiled length range.
- Hole in an array.
const a = [1, 2, 3]; a[10] = 4;— V8's array elements become "holey", which has a slower fast path. delete obj.x. Forces the object into dictionary mode — kills the hidden class.
7 · Practical implications
- Initialise all object fields in the constructor in a consistent order. No conditional
this.x = .... - Avoid
deletein hot paths. Set toundefinedornullinstead. - Keep arrays packed. Don't assign past the length; don't create holes.
- Pass the same shapes to the same functions. Polymorphic-but-stable beats megamorphic.
- Avoid
argumentsin hot code. Use rest parameters instead. - Stable function arity. Always call a function with the same number of arguments.
8 · What the optimiser inlines
TurboFan inlines aggressively — a small enough callee with stable type feedback gets
inlined into its callers. Array methods like .map, .filter,
and .forEach are often inlined; the callback is too, if it's
monomorphic-stable. This is part of why modern JS doesn't pay a meaningful cost for
higher-order functions in hot loops.
References
- Launching Ignition and TurboFan — V8 team — the original announcement.
- Sparkplug — a non-optimizing JS compiler — The third tier added in 2021.
- mrale.ph — What is up with monomorphism? — The definitive monomorphism primer.
- v8-internal.h — Hidden-class and IC types in the V8 source.
- Benedikt Meurer — Performance issues in modern JS — Talk from a V8 engineer on common foot-guns.