Internals
Internals

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.

text bytecode
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  Return

The [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.

js hidden-classes.js
// 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.
Why it matters. Property reads at a call site whose objects all share one hidden class are monomorphic — V8 inlines the offset lookup. If the call site sees many shapes, it becomes polymorphic, then megamorphic, and each state is slower than the last.

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 stateShapes seenCost
uninitialised0full lookup
monomorphic1cached offset — fast
polymorphic2–4switch on shape — slower
megamorphic5+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

shell terminal
# 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.js

6 · 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 delete in hot paths. Set to undefined or null instead.
  • 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 arguments in 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

Found this useful?