awesome-everything RU
↑ Back to the climb

Browser & Frontend Runtime

Hidden classes, transition trees, and memory layout

Crux How V8 tracks object shapes via hidden classes, why property-addition order matters, in-object vs out-of-object slots, array element kinds, and the dictionary-mode trap.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at middle altitude — in the sky
◷ 14 min

Two objects have the same properties. One was built with {x:1, y:2}, the other with {y:2, x:1}. To your code they look identical. To V8 they are different shapes — and whichever one ends up in a hot function first decides whether that function runs fast or slow.

What a hidden class is

JavaScript objects are dynamic — properties can be added, deleted, reordered — but V8 internally represents each object as a pointer to a hidden class (called “Map” in V8 source, “Shape” in SpiderMonkey) plus a sequence of property slots. Two objects with the same property names added in the same order share one hidden class.

When you add a property to an existing object, V8 transitions to a new hidden class along a “transition tree”. Adding “x” then “y” goes to one class; adding “y” then “x” goes to a different one — even though both objects have the same logical contents. The inline cache keys off the hidden class pointer. Stable hidden classes are the foundation of V8 performance.

Rules:

  • Construct objects with all properties up front (in constructors or factory functions).
  • Avoid delete on object properties — it forces a transition to a dictionary-mode object that is permanently slow.
Hidden class and memory layout numbers
In-object property slots (default)
~8 slots
Out-of-object: extra pointer dereference
+1 memory load
Dictionary mode threshold (~deletions)
~32+ properties or many deletes
Smi range (64-bit)
±2³¹
Smi arithmetic cost
1 CPU instruction, no allocation
HeapNumber cost
1 heap allocation + dereference

The transition tree

const p = {}; p.x = 1; p.y = 2;
// HC path: HC_empty → HC_x → HC_x+y

const q = {}; q.x = 1; q.y = 2;
// Same path — q ends at same HC as p. ✓ monomorphic

const r = {}; r.y = 2; r.x = 1;
// HC path: HC_empty → HC_y → HC_y+x
// Different leaf — different HC from p and q. ✗ polymorphic at shared call site

The transition tree is shared across all objects in the V8 isolate. Pathological code that adds properties in dynamic order (JSON parsers writing properties in arbitrary input order, generated code) creates many leaf nodes — the same shape never repeats, ICs are perpetually polymorphic or megamorphic.

In-object vs out-of-object properties

V8 stores the first ~8 properties of an object inline in the object’s memory block. Subsequent properties go to an out-of-object PropertyArray pointed to by the object. Access to in-object properties is a single memory load; out-of-object adds one pointer dereference.

The in-object slot count is fixed at hidden class creation. For perf-critical objects, declare the most-accessed properties first in the constructor so they land in-object.

Dictionary mode: objects with too many properties (~32+) or many deletions transition to a HashMap-backed representation where every access goes through the slow generic path. Once in dictionary mode, the object is permanently slow — %HasFastProperties(obj) in d8 reports false.

Array element kinds

V8 tracks the “kind” of each array based on what it stores. Transitions are one-way — once you move to a wider kind, you cannot go back:

KindContentsSpeed
PACKED_SMI_ELEMENTSonly smi integers, no holesfastest
PACKED_DOUBLE_ELEMENTSonly doubles, no holesfast
PACKED_ELEMENTSmixed types or objectsnormal
HOLEY_* variantssparse array (has holes)adds hole-check per access

new Array(1000) creates a HOLEY array from the start. Array.from({length:1000}, () => 0) creates a PACKED array. Same logical result, 2–3× different access speed.

To keep an array PACKED_SMI: pre-allocate with [], add via push, never assign at indices above the current length.

Numeric specialisation: Smi, HeapNumber, double

V8 represents small integers (Smi) inline in a 64-bit pointer slot with the low bit cleared — no allocation, no indirection, arithmetic in one CPU instruction. Numbers outside the Smi range (±2³¹ on 64-bit) become HeapNumbers — boxed in a heap-allocated cell with a double inside. The Smi-to-HeapNumber transition is V8’s most common deopt trigger: a TurboFan-compiled loop assumes Smi, the value overflows once, deopt cascade. The fix: bound the integer range, or use Math.fround / explicit Float64Array.

Quiz

Two objects: `{x:1, y:2}` and `{y:2, x:1}`. A function accesses `.x` on both. What IC state does the access site reach?

Order the steps

Order the hidden-class transitions for: const o = {}; o.x = 1; o.y = 2; o.z = 3;

  1. 1 HC_empty (object created with no properties)
  2. 2 HC_x (property 'x' added)
  3. 3 HC_x+y (property 'y' added)
  4. 4 HC_x+y+z (property 'z' added)
Quiz

Why does `new Array(1000)` create a slower array than `[]` followed by 1000 pushes?

Edge cases

String interning: V8 maintains an internal pool for small string literals and property names. Two interned strings compare as pointer equality (1 cycle), not char-by-char. Concatenation creates a ConsString — a flat tree without copying. Substring creates a SlicedString holding a reference to the parent string. This gives O(1) substring/concat, but the parent string stays alive in the heap as long as any slice exists. For memory-sensitive code, str.slice() + '' or explicit copy breaks the reference chain, freeing the parent.

Recall before you leave
  1. 01
    What is the 'hidden class transition tree' and why does property order at creation matter?
  2. 02
    What is 'dictionary mode' and how do you trigger it?
  3. 03
    Why does the Smi-to-HeapNumber transition cause a deopt in TurboFan?
Recap

Every V8 object carries a pointer to a hidden class that describes exactly which properties exist and at which memory offsets. When you add a property to an object, V8 follows or creates a transition edge in the hidden class transition tree — the path depends on the order of addition, not just the final set of properties. Two objects with the same properties added in different orders sit at different leaf nodes and look like different shapes to any inline cache. In-object properties (first ~8) cost one memory load; out-of-object ones cost two. Dictionary mode — triggered by delete or excessive dynamic property addition — replaces the fixed-offset layout with a hashmap and is permanent. Array element kinds follow a one-way widening path: PACKED_SMI is fastest, HOLEY variants require a hole-check on every access. Smi arithmetic is allocation-free; overflowing the Smi range creates a HeapNumber and triggers a TurboFan deopt.

Connected lessons
appears again in162
Continue the climb ↑Inline caches, IC states, and deoptimization
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.