V8 Hidden Classes: Map Transitions and Object Shape Design
V8 uses hidden classes called maps to give dynamic JavaScript objects fixed property offsets, but unstable shapes can force slow dictionary mode.
01 Oct 2026, 14:44 UTC

JavaScript lets code add or delete properties at any time, but reading a property through a general hash-table lookup on every access is too slow for hot code. V8 solves this by assigning each object a hidden class, internally called a map, that records the object's shape: which properties exist, where they sit in memory, and what map comes next when a property is added. Objects with the same shape share the same map, so property access becomes a fixed-offset load guarded by an inline cache.
The useful takeaway for engineers is that map sharing depends on consistent property addition order. If objects acquire properties in different sequences, or if code mutates prototypes frequently, V8 may abandon maps entirely and move the object into dictionary mode, which loses the offset-based speed benefit.
Requirements for Object Shape Tracking
The map system must satisfy three constraints at once:
- Fast property access: reads should resolve to a memory offset, not a string comparison loop.
- Memory sharing: thousands of objects with identical shapes should reference one map object instead of storing duplicate metadata.
- Dynamic flexibility: the engine must support adding and deleting properties without invalidating already compiled code incorrectly.
Smallest Suitable Design: The Map Transition Tree
A map is an immutable object inside the V8 heap. It stores:
- A mapping from property names to offsets within the object's property storage.
- A pointer to the object's prototype.
- A transition table that names the next map for a given property addition.
Every object begins at an empty map. When code assigns a property, V8 checks the current map's transition table. If a transition for that property already exists, the object's map pointer is updated to the existing target map. If not, V8 creates a new map with the new property appended at the next offset and records the transition. The old map is never mutated.
This produces a transition tree rooted at the empty map. Two objects that add a then b converge on the same map instance. Two objects that add b then a follow a different path and receive a different map, even though the final property set is identical.
Trust and Data Boundaries
Maps are engine-internal. They live only in the V8 heap and are never exposed to JavaScript. A script cannot read a map pointer, forge a map, or edit a transition table. This boundary matters because compiled code assumes that a given map implies a fixed memory layout. If JavaScript could fabricate a map, it could point an object at a map whose offsets do not match the object's actual storage, causing out-of-bounds reads or type-confusion exploits.
The map pointer itself is stored in the object header and is updated only by V8's internal property operations. External embeddings that interact with V8 through the C++ API must treat map addresses as opaque engine handles.
Operational Checks: Inline Caches and Garbage Collection
When V8 compiles property access, it emits an inline cache. The cache records the map it observed and the offset to load. On the next execution, if the object's map matches the cached map, V8 loads the property directly at the offset. If the map differs, the cache misses, V8 looks up the property through the map chain, and may patch the cache or trigger deoptimization if the shape is too unstable.
During garbage collection, V8 traverses maps and their transition pointers. It verifies that transition targets are valid heap objects and that no object references a map incompatible with its allocated storage. A corrupt transition pointer would otherwise let the engine read an offset into the wrong memory region.
Failure Modes and Conditions That Change the Design
Transition explosion
If many objects add the same properties in different orders, the transition tree branches for every permutation. Each branch holds a distinct map, increasing metadata memory and reducing the chance that objects share a map.
Dictionary mode
When an object undergoes too many shape changes, or when properties are deleted frequently, V8 converts the object to dictionary mode. The object then stores properties in a hash table instead of fixed slots. Property access becomes slower, and inline caches for that object typically deoptimize because there is no stable map to cache.
Prototype mutation
Changing an object's prototype after its map exists can invalidate the map's assumptions about inherited properties. V8 may transition the object to a new map or deoptimize code that relied on the old layout. Frequent prototype changes in hot loops amplify this cost.
Observing Map Transitions in d8
You can inspect map behavior locally with V8's d8 shell. This is a debug tool, not a production configuration, and --allow-natives-syntax exposes internal functions that can crash the process if misused.
Run the command from a directory containing the V8 d8 binary. No special operating-system permissions are required beyond executing the binary.
d8 --allow-natives-syntax inspect.js
Example inspect.js contents, with obj as a placeholder for the object under test:
var obj = {};
%DebugPrint(obj);
%HasFastProperties(obj);
obj.first = 1;
%DebugPrint(obj);
obj.second = 2;
%DebugPrint(obj);
Expected checks:
%DebugPrintprints the map address for each object state.- After each property addition, the map address should change to a new map, while the previous map remains at its original address.
%HasFastProperties(obj)returns true while the object stays in fast-property mode and false after a transition to dictionary mode.
Risk: do not run scripts with --allow-natives-syntax from untrusted sources. The native-syntax functions can manipulate internal V8 state and may terminate the shell.
Limitations and Verification
The immutable-map model described here applies to recent stable V8 releases. Older versions may use different transition heuristics or store property metadata differently. Observed map addresses are implementation details and can change between V8 versions; do not depend on them in application code.
To verify behavior in your environment, run the d8 snippet above on the target V8 release and confirm that map addresses change on property addition and that %HasFastProperties flips to false only after repeated or irregular shape changes. If an object enters dictionary mode sooner than expected, review property addition order and prototype assignments in the hot path.
When the Design Should Change
An architecture should move away from the pure transition-tree map model if profiling shows that most objects never share maps because every instance has a unique shape. In that regime, the memory cost of maintaining transitions outweighs the inline-cache benefit, and a dictionary-first or hybrid layout may be preferable. Similarly, if a workload relies on prototype mutation as a primary dispatch mechanism, the invalidation cost of map changes can dominate execution time, suggesting a design that avoids prototype changes after object initialization.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.