V8 Engine · Lesson 7

Hidden Classes & Inline Caches

Why this lesson: Your mission includes explaining hidden classes and inline caches well enough to predict why a JS pattern is fast or slow. This lesson cashes out the wrong map cliffhanger from Lesson 2 — a Map is V8's hidden class, and it's the single biggest reason idiomatic JS runs near native speed.

Here's the puzzle V8 has to solve. In a static language, point.x compiles to "read memory at offset 8" — instant. But JavaScript objects are dynamic dictionaries: you can add or delete properties any time. A naive engine would do a hash-table lookup on every point.x. That's death by a thousand cuts. V8's answer is the hidden class.

The core trick: V8 secretly groups objects by shape. Every object has a hidden pointer to a Map (V8's name for a hidden class) that describes its layout — which properties it has and at what fixed offset each lives. Objects built the same way share the same Map. Now point.x can compile to "if the Map is the one I expect, read offset 8" — almost as fast as the static language.

Same construction → same hidden class

Two objects have the same Map when they have the same properties, added in the same order. Let's prove it with %HaveSameMap (needs --allow-natives-syntax):

function Point(x, y) { this.x = x; this.y = y; }
const a = new Point(1, 2);
const b = new Point(3, 4);   // same constructor, same order
const c = { x: 5, y: 6 };    // literal: same property names & order...
const d = { y: 8, x: 7 };    // ...but reversed order
console.log(%HaveSameMap(a, b));  // ?
console.log(%HaveSameMap(a, c));  // ?
console.log(%HaveSameMap(a, d));  // ?
c.z = 99;                      // add a property after the fact
console.log(%HaveSameMap(a, c));  // ?

Run with node --allow-natives-syntax shapes.js. The actual output on a current V8:

a vs b same map: true     // identical construction → shared Map
a vs c same map: false    // constructor objects ≠ plain literals here
a vs d same map: false    // property ORDER changes the shape
a vs c after z:  false    // adding a property transitioned c to a NEW Map

Two lessons jump out: property order matters ({x,y} and {y,x} are different shapes), and adding a property changes the shape. That second one is the key to transitions.

The same layout, three objects, one Map

Picture what a and b share. The Map isn't stored in each object — it's a separate descriptor they both point to. Each object only stores the values, in slots:

Object a

xoffset 0
yoffset 1
→ Map M1

Object b

xoffset 0
yoffset 1
→ Map M1

Map M1 (shared)

"x" →offset 0
"y" →offset 1
the shape, stored once
See it directly with %DebugPrint. Running %DebugPrint({x:1, y:2}) prints the object's internals, including its Map:
DebugPrint: 0x..: [JS_OBJECT_TYPE]
 - map: 0x..<Map[40](HOLEY_ELEMENTS)> [FastProperties]
 - properties: <FixedArray[0]>
FastProperties means V8 is using the hidden-class fast path. If an object's shape becomes too chaotic, V8 gives up and switches it to dictionary mode (a real hash table) — the slow path you want to avoid. Source: Fast properties in V8.

Transitions: shapes form a tree

When you add z to an object shaped {x, y}, V8 doesn't mutate the Map — it transitions to a new Map {x, y, z} and records the link. The next object that goes {x, y} then adds z follows the same transition and ends up sharing that Map. This is why constructing objects the same way is so valuable: everyone walks the same path through the transition tree and shares Maps at every step.

{} ──add x──▶ {x} ──add y──▶ {x,y} ──add z──▶ {x,y,z}
                              ▲ a, b live here   ▲ c ends up here after c.z=99

Canonical reference: Maps (Hidden Classes) in V8 · best conceptual intro: Shapes and Inline Caches (Bynens & Meurer).

Inline Caches: remembering where things live

Hidden classes make a fast lookup possible; inline caches (ICs) are what make it actually fast. Every property-access site in your code (like the o.x in a function) gets a little cache attached. The first time it runs, V8 does the full lookup and records: "for Map M1, property x is at offset 0." Every subsequent call just checks "is this still M1? yes → grab offset 0." That check-and-grab is the recorded type feedback from Lesson 1, and it's what the optimizing compilers turn into a couple of machine instructions.

How many shapes an IC has seen determines its speed:

Monomorphic · fast1 shape ever seen. The ideal: one Map check, direct offset read. Optimizers love this.
Polymorphic · slower2–4 shapes. V8 checks a small list of Maps. Still OK, but the optimizer must hedge.
Megamorphic · slowMany shapes. V8 gives up caching here and falls back to a generic lookup. This is the perf cliff.
This is exactly the Lesson 2 deopt. When load(o) was optimized assuming Map M1 and then met an object with a different Map, the IC's assumption broke → reason: wrong map → deoptimization. Hidden classes and inline caches are two sides of the same coin: the Map is the shape; the IC is the bet on that shape.

The payoff: why some JS is fast and some isn't

Do this (stays monomorphic)Avoid this (goes poly/megamorphic)
Initialize all properties in the constructor, always in the same order.Adding properties conditionally or later (if (x) obj.foo = …) → shape forks.
Give every object of a "type" the identical set of fields.delete obj.prop → often drops the object to dictionary mode.
Keep a function receiving one object shape.Passing many different shapes to one function → megamorphic call site.

You can now predict performance: code that keeps shapes stable keeps its inline caches monomorphic and its optimized code alive. Code that mutates shapes forces polymorphism and deopts. That's the whole game.

Check yourself feedback loop

1. Why does {x:1, y:2} have a different hidden class than {y:2, x:1}?
2. What is an inline cache (IC) caching?
3. A function is called with 50 differently-shaped objects at one property access. That call site becomes…
4. You profile a hot function and see constant deopts with reason: wrong map. Best first fix?
Ask me anything. Want to watch a call site go monomorphic → megamorphic with a live experiment? Curious how arrays get their own version of this (elements kinds)? Want to see %DebugPrint on a dictionary-mode object vs a fast one? Just ask — e.g. "Show me code that forces dictionary mode," or "How do ICs interact with prototypes?"

Where this lands you

You can now explain V8's object model — Maps, transitions, FastProperties vs dictionary mode — and inline caches, and use them to predict performance. Combined with the tier/deopt model from Lesson 2, you have the full "why JavaScript gets fast" story. The last internals lesson covers the other half of the runtime: how the garbage collector reclaims all those objects without freezing your program.

Reference: V8 Glossary (updated) · Inspection Flags · Related: Lesson 2 (deopt)

V8 Engine learning track · Lesson 7 · Built around your mission: predicting JS performance from engine internals.