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.
point.x can compile to "if the Map is the one I expect,
read offset 8" — almost as fast as the static language.
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.
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:
ab%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.
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).
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:
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.
| 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.
{x:1, y:2} have a different hidden class than {y:2, x:1}?reason: wrong map. Best first fix?%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?"
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)