Under the Hood
Jsengine

Hidden classes and inline caches

A JavaScript object looks like a free-form dictionary, but V8 secretly assigns it a hidden class describing a fixed memory layout and caches every property lookup against that layout, which is the mechanism that lets dynamic objects run at near-struct speed and the reason the order you add properties in actually matters.

Hidden classes and inline caches

Nothing in JavaScript stops you from writing obj.newProp = 5 on any object at any time, which makes a JS object sound like it must be, under the hood, a hashmap — a bag of key-value pairs you can grow or shrink at will. If V8 actually implemented objects that way, every single property access would mean hashing a string key, probing a table, and following a pointer to find the value, on every read and every write, for every object in the program. Real engines don't do this for the common case, and the reason is one of the more elegant pieces of the whole V8 design: the hidden class.

The problem with treating every object as a hashmap

A hashmap is flexible but expensive per-access: computing a hash, resolving collisions, following indirection — all of that work happens again every single time you touch obj.x, no matter how many times you've touched it before. It's also memory-heavy, because a general hashmap has to store the keys themselves alongside the values, not just the values.

But most JavaScript code doesn't actually use objects this dynamically. A function that creates points does { x: 1, y: 2 } the same way every time it runs. A class's constructor assigns the same fields in the same order on every instantiation. The shapes objects take in real programs are far more repetitive than the language's rules require them to be — which is exactly the kind of regularity lesson 1 said the whole engine is built to discover and exploit.

Hidden classes: giving a dynamic object a fixed layout

V8 exploits that regularity by giving every object a hidden class (V8 calls it a Map internally; other sources call it a Shape) — a separate, shared data structure that records which properties the object has and the exact memory offset where each property's value lives. The object itself then stores just an array of values in that fixed layout, plus a pointer to its hidden class. Reading obj.x becomes: look up "x" in the hidden class once to get an offset, then read that offset out of the object's value array — structurally the same operation as reading a field out of a C struct, not a hashmap probe.

Because the hidden class is a separate, shareable object, two JavaScript objects that end up with the exact same properties, added in the exact same order, point at the same hidden class — they don't each pay to construct their own layout description; they share one.

function Point(x, y) {
  this.x = x;
  this.y = y;
}

const a = new Point(1, 2); // x then y -> hidden class H_xy
const b = new Point(3, 4); // x then y -> also H_xy, shared with a

Transitions: why the order you add properties matters

An object doesn't get its final hidden class the moment it's created — it starts from an empty hidden class and transitions to a new one every time a property is added, in the order those properties are added. V8 builds these transitions into a tree rooted at the empty shape:

Two objects that both end up with properties x, y, and z do not necessarily share a hidden class — they only do if they added those properties in the same order. {x: 1, y: 2} and {y: 2, x: 1} have the same properties and the same final set of values, but they walk different branches of the transition tree and land on different hidden classes, because the tree records the path taken, not just the destination.

function makeInOrder() {
  const obj = {};
  obj.x = 1; // transition: {} -> {x}
  obj.y = 2; // transition: {x} -> {x, y}
  return obj;
}

function makeOutOfOrder() {
  const obj = {};
  obj.y = 2; // transition: {} -> {y}
  obj.x = 1; // transition: {y} -> {y, x}
  return obj;
}

const p1 = makeInOrder();
const p2 = makeInOrder();     // shares p1's hidden class
const p3 = makeOutOfOrder();  // different hidden class from p1 and p2,
                               // even though p1, p2, and p3 all have
                               // exactly the same own properties

This is the mechanical reason behind the common advice to always initialize every field in a constructor, in the same fixed order, rather than conditionally adding properties later based on runtime logic. Do that consistently and every instance walks the identical transition path and lands on the identical hidden class; branch the order (or add fields outside the constructor, sometimes and not others) and instances of what looks like "the same object type" fragment across multiple hidden classes.

The playground below builds objects with the same properties in the same order versus a different order, so you can watch them share or diverge on a hidden class live.

Scenario
Speed
+x+yC0{}C1{x}C2{x, y}objAobjB
Two fresh objects share the empty shape C0.

V8 gives every object a hidden class describing its property layout. Adding a property transitions the object to a new hidden class along an edge named after that property. Objects that add the same properties in the same order walk the same edges and end up sharing a hidden class — property access at a call site seeing only that shape is monomorphic and fast. Add properties in a different order and you diverge onto a different branch of the tree: the object ends up with the same properties but a different shape, and a call site that sees both becomes polymorphic and slower. Initialise object properties in a consistent order to keep shapes — and access sites — monomorphic.

Inline caches: remembering what you found last time

Sharing a hidden class solves the layout problem, but there's a second piece: a specific line of code like obj.x gets executed repeatedly (in a loop, or across many calls to the same function), and V8 doesn't want to redo "look up x in the hidden class to get an offset" from scratch every single time either. So each property-access site in the compiled code keeps an inline cache (IC) — literally, a small cache stored inline at that site — recording "the last time I saw a hidden class H here, x was at offset N." The next time that exact site runs, it checks whether the incoming object's hidden class is still H; if so, it skips straight to reading offset N, no lookup at all.

This is precisely the monomorphic / polymorphic / megamorphic spectrum lesson 4 introduced for the JIT's guards, and it isn't a coincidence — it's the same underlying concept (how many distinct shapes has this site seen) applied to property access instead of type speculation:

  • Monomorphic IC — the site has only ever seen one hidden class. It caches a single offset and every subsequent hit is a direct read. This is the fast path the whole hidden-class system exists to produce.
  • Polymorphic IC — the site has seen a handful of different hidden classes (again, a small tracked number). The IC holds a short list of "if hidden class is H1, offset is N1; if H2, offset is N2; ..." and checks each in turn. Slower than monomorphic, still much faster than a full lookup.
  • Megamorphic IC — the site has seen more hidden classes than V8 will track per-site. At that point the IC gives up on caching this site's history at all and falls back to a slower, generic property lookup shared across the whole program.

A loop that always accesses .x on objects sharing one hidden class keeps that access site monomorphic and essentially free after the first hit. A loop that accesses .x on objects arriving with three or four different property orderings pushes that same site toward polymorphic or megamorphic — the code hasn't changed at all, only the shapes flowing through it have, and that alone is enough to slow it down.

Falling out of the system entirely: dictionary mode

The hidden-class scheme assumes objects mostly grow in predictable ways. Some patterns break that assumption badly enough that V8 gives up on it for a particular object and drops that object into dictionary mode — a real hashmap, property names and all, exactly the structure hidden classes exist to avoid. Deleting a property is the classic trigger (there's no clean "un-transition" back up the tree), and so is adding a very large number of properties dynamically and unpredictably, especially past a size where maintaining transition-tree entries for it stops being worth the bookkeeping. An object in dictionary mode loses the fixed-offset fast path and every inline cache pointed at it falls back to a slow, generic lookup. It's the object-shape equivalent of a call site going megamorphic — the system that makes property access fast has an escape hatch, and the escape hatch is intentionally slow, because it's meant to be rare.

Where this goes next

Hidden classes and inline caches are how V8 makes shape cheap to check and layout cheap to use. The other half of "cheap to use" is what actually lives in each of those fixed offsets — and JavaScript numbers turn out to have their own trick for staying fast, hiding small integers directly inside a pointer-sized value instead of allocating a box for every one. Value representation picks that up next.

Go deeper

Check yourself

Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.

  1. Why is treating every JavaScript object as a hashmap slow, and what regularity does V8 exploit instead?
  2. What does a hidden class actually store, and what does the object itself store once it has one?
  3. Two objects end up with the same final properties but were built by adding those properties in a different order. Do they share a hidden class? Why or why not?
  4. What is a hidden-class transition, and why do transitions form a tree rather than a single chain?
  5. What does an inline cache store at a property-access site, and what happens on the next access if the incoming object's hidden class matches what's cached?
  6. Define monomorphic, polymorphic, and megamorphic for an inline cache, and connect these terms back to the same spectrum from the JIT lesson.
  7. What is dictionary mode, what commonly triggers it (name at least one cause), and what does an object lose by falling into it?