Hidden Classes

Hidden Classes, Inline Caches and Deoptimization

A hash-table lookup is slow, yet point.x in V8 86,723 is usually one memory load thanks to the hidden class, called a Map in V8 (a Shape in SpiderMonkey 946,470 ). Every object's first word points to a Map that records which properties exist and at which offset. Objects built with the same properties in the same order share a Map; adding a property moves the object along a transition to a new Map, and delete usually drops it into slow dictionary mode.

Each property access site has an inline cache (IC) that remembers the Maps it has seen and their offsets. With one Map the site is monomorphic and optimized code is just a Map check plus a load. V8 tracks up to four Maps (polymorphic); beyond that the site turns megamorphic and uses a slower generic lookup.

Hidden-class transitions (left) and the states of an inline cache (right)
Hidden-class transitions (left) and the states of an inline cache (right)

With node --allow-natives-syntax, V8's internal %HaveSameMap(a, b) confirms that {x: 1, y: 2} and {y: 5, x: 6} have different Maps. The cost appears when one access site sees many Maps; this benchmark sets their number from the command line:

Measuring monomorphic, polymorphic and megamorphic access (run: node ic.mjs 5)JavaScript
const shapes = Number(process.argv[2] ?? 1);
const points = Array.from({ length: 10_000 }, (_, i) =>
  ({ ['k' + (i % shapes)]: 0, x: i }));        // a different leading key per Map
function sumX(list) {
  let total = 0;
  for (let i = 0; i < list.length; i++) total += list[i].x;   // one inline cache
  return total;
}
sumX(points);                                  // warm up, collect type feedback
const t0 = performance.now();
for (let r = 0; r < 5_000; r++) sumX(points);
console.log(`${shapes} shape(s): ${(performance.now() - t0).toFixed(0)} ms`);
Output
1 shape(s): 45 ms
2 shape(s): 69 ms
4 shape(s): 98 ms
5 shape(s): 245 ms
8 shape(s): 246 ms

Across five runs on Node.js 25.8 2,131 , timings vary but the cliff between four and five Maps (V8's --max-valid-polymorphic-map-count=4) is typical. Deoptimization is the other cliff: when an optimized guard fails (a string arrives where integers were seen), V8 discards the code and resumes in the interpreter (How V8 Executes Code). So initialize every field in the constructor in one order, avoid delete, keep argument types stable in hot functions, and profile first (Profiling and Source Maps): most code is never hot enough to matter.