63. Shapes in depth
Two objects with the same properties can look completely different to V8. It depends on the order you added them, how you made the object, and whether you ever used delete.
- Explain what a shape (V8 calls it a map) stores, and what an object stores itself.
- Trace how a transition tree grows as properties are added, and why a different order gives a different shape.
- Read real %DebugPrint output for an object and its map.
- Explain dictionary mode, what triggers it, and why it is slower to read.
Same keys, different objects
Here are four objects with the same two properties, and a V8 internal,
%HaveSameMap, that asks whether two objects have the same shape:
// node --allow-natives-syntax shapes.js
const a = { x: 1, y: 2 };
const b = { x: 5, y: 6 };
const c = { y: 2, x: 1 };
const d = {}; d.x = 1; d.y = 2;
const e = {}; e.x = 7; e.y = 8;
console.log(%HaveSameMap(a, b));
console.log(%HaveSameMap(a, c));
console.log(%HaveSameMap(d, e));
console.log(%HaveSameMap(a, d));
Node v22.23.4, V8 12.4.254.21
true
false
true
false
a and b match. a and c don’t, though they differ only in order.
And a and d don’t either, though they have the same keys in the same
order. By the end of this chapter all four answers will be obvious.
A shape is a shared description
A school prints one form for every pupil: name in box 1, class in box 2. Each pupil’s sheet holds only their answers. To find someone’s class you don’t read the whole sheet, you look at box 2, because the form says so. Thousands of sheets share one description of the layout.
Chapter 22 showed that every V8 object starts with a pointer to its hidden class. Engine writers also say shape, and V8’s own name is map[2]. The map holds the layout: which named properties exist, in what order, and where each value is stored. The names themselves live in the map’s descriptor array, not in the object[1]. The object holds only the values.
Compare Drip. In chapter 59 every Drip instance kept its own hash table of
fields, names and all. Ten thousand Drip particles meant ten thousand
tables, each storing "x" and "y" again. V8 stores the names once, in a
map, and shares it.
Shapes form a tree
JavaScript has no fixed classes: any object can gain a property at any time. So V8 can’t know a shape in advance. Instead, each time an object gets a new property, it moves to a new shape, and V8 links the shapes together[1]. Step through it:
const a = {};a.x = 1;a.y = 2;const b = {};b.x = 5;b.y = 6;const c = {};c.y = 2;c.x = 1;
The tree is what makes sharing work. A second object built the same way follows the same transitions and ends at the same shape. An object built in a different order walks down a different branch. In V8’s source each map keeps a back pointer to the map it came from[3], and you can see it in V8’s debug output:
// %DebugPrint(d), trimmed to fit
DebugPrint: 0x37f774024609: [JS_OBJECT_TYPE]
- map: 0x36122091bff1 <Map[56]...>
[FastProperties]
- All own properties ...: {
#x: 1 (const data field 0) ...
#y: 2 (const data field 1) ...
}
0x36122091bff1: [Map] in OldSpace
- instance size: 56
- inobject properties: 4
- unused property fields: 2
- back pointer: 0x36122091bfa9 <Map[56]...>
A transition tree links shapes: the root is the empty shape, and each edge says “add this property”. Adding a property follows an existing edge if there is one and creates a new shape if not. Same properties in the same order lead to the same shape; a different order leads to a different branch[1].
That explains a against c. And a against d? Look at d’s map
above: four in-object slots, two of them unused. d started as {},
and in this run V8 gave the empty object room for four properties. The
literal { x: 1, y: 2 } got exactly two slots: %DebugPrint(a) in the
same session showed inobject properties: 2. Different layouts, so
different maps.
const a = { x: 1, y: 2 };
const c = { y: 2, x: 1 };
console.log(Object.keys(a));
console.log(Object.keys(c));
console.log(JSON.stringify(a));
console.log(JSON.stringify(c));
// Code can see the order, so an
// engine must remember it: that is
// why order is part of a shape.Room to grow: slack tracking
Objects made by a constructor get the same trick, more generously. In 2020 the V8 team described slack tracking: a new object gets extra in-object space, and after seven constructions V8 shrinks the shape to what was actually used. The starting estimate is the properties the compiler expects, plus eight[2]. You can watch it:
function P(x, y) { this.x = x; this.y = y; }
%DebugPrint(new P(1, 2));
for (let i = 0; i < 10; i++) new P(i, i);
%DebugPrint(new P(3, 4));
Node v22.23.4, V8 12.4.254.21
// first object
- instance size: 104
- inobject properties: 10
- unused property fields: 8
- construction counter: 6
// after eleven more
- instance size: 40
- inobject properties: 2
- unused property fields: 0
- construction counter: 0
Ten slots (two expected, plus eight), then two: 104 bytes per object became 40.
When V8 gives up: dictionary mode
Shapes pay off when many objects share a few layouts. If a program keeps adding and deleting properties, maintaining all those shapes costs more than it saves. So V8 can switch an object to slow properties: a self-contained hash table that holds names and values together, like a Drip instance[1].
const p = { x: 1, y: 2, z: 3 };
p.z = undefined;
console.log(%HasFastProperties(p));
delete p.z;
console.log(%HasFastProperties(p));
let last = true;
for (let n = 1; n <= 40; n++) {
const o = {};
for (let i = 0; i < n; i++) {
o['k' + i] = i;
}
const f = %HasFastProperties(o);
if (f !== last) console.log(n, f);
last = f;
}
Node v22.23.4, V8 12.4.254.21
true
false
20 false
Setting p.z to undefined kept the shape. delete turned p into a
dictionary, and its map now says dictionary_map. Adding properties
under computed names turned an object into a dictionary at 20 properties
in this version. Don’t memorize that number: it is an engine detail.
An object in dictionary mode stores its named properties in its own hash table instead of using a shared shape. Adding and removing properties is cheap, but reads are slower, and the inline caches of the next chapter don’t work on it[1]. V8’s source says a map is used either for dictionary-mode objects or for fast ones, never both[3].
delete obj.prop is fine for a lookup table you add and remove keys from.
For an object with a fixed set of fields, such as a particle, set the field
to undefined (or keep a flag) instead. Better still, use a Map for data
whose keys change.
Shapes are why the optimizing tiers can be fast. In 2023 the V8 team
described Maglev reading o.x: if feedback says o always had one shape,
it emits a check of the shape and then a plain load at a fixed
offset[5]. A shape change can also widen a field’s
representation, as you saw with the TypeScript class in chapter
22[4]. For Viscosity, the lesson is the one the
benchmarks taught: build every element of a collection the same way, in the
same order, and V8 sees one shape. A struct-of-arrays layout goes further:
a Float64Array column has no shapes at all.
Your turn
A shape is { keys, next }: keys lists property names in order, and next is a Map from a property name to the child shape. Write addProp(shape, key), which returns the shape after adding key. If shape already has a transition for key, return that child; otherwise create it, record it in shape.next, and return it. shapeOf and slotOf are given.
const ROOT = {
keys: [], next: new Map(),
};
function addProp(shape, key) {
return {
keys: [...shape.keys, key],
next: new Map(),
};
}
function shapeOf(keys) {
let s = ROOT;
for (const k of keys) {
s = addProp(s, k);
}
return s;
}
function slotOf(shape, key) {
return shape.keys.indexOf(key);
}Write orderMismatches(objs). For each object, look at its keys (Object.keys). If an earlier object had the same set of keys in a different order, the object is a mismatch. Return the indexes of all mismatches, in order. Objects with different sets of keys never mismatch each other.
function orderMismatches(objs) {
return [];
}What you learned
- A shape (V8’s map) describes an object’s layout once; objects that share it store only their values.
- Shapes form a transition tree: adding a property follows or creates an edge, so the order of additions decides the shape.
- How an object is made matters too: literals,
{}plus additions and constructors get different in-object space, and constructors shrink theirs after a few objects (slack tracking). - Dictionary mode replaces the shared shape with a per-object hash
table.
deletetriggers it here; reads get slower. - Build objects of one kind the same way, in the same order, and don’t
deletetheir fields.
Next: how V8 uses shapes to make o.x fast at one particular line of code.
Check your understanding
0/6- 1.
What does a V8 object store itself, and what does its map store?
- 2.
const a = {}; a.x = 1; a.y = 2;and thenconst b = {}; b.x = 3; b.y = 4;. How many new shapes doesbcreate? - 3.
Why does the order of property additions matter to V8?
- 4.
In the chapter’s run, the first
new P(1, 2)had 10 in-object slots and a later one had 2. Why? - 5.
What is dictionary mode?
- 6.
You have 100,000 particle objects and want to mark some as dead. Which keeps them fast?
References
- [1] Camillo Bruni (2017). Fast properties in V8. V8 blog (v8.dev), 30 August 2017. v8.dev/blog/fast-properties (checked 2026-10-31)
- [2] Michael Stanton (2020). Slack tracking in V8. V8 blog (v8.dev), 24 September 2020. v8.dev/blog/slack-tracking (checked 2026-10-31)
- [3] The V8 project authors (2024). V8 source code at tag 12.4.254.21: src/objects/map.h. V8 source repository (github.com/v8/v8), tag 12.4.254.21 (the V8 inside Node v22.23.4). github.com/v8/v8/blob/12.4.254.21/src/objects/map.h (checked 2026-10-31)
- [4] Benedikt Meurer, Mathias Bynens (2019). The story of a V8 performance cliff in React. V8 blog (v8.dev), 28 August 2019. v8.dev/blog/react-cliff (checked 2026-10-31)
- [5] Toon Verwaest, Leszek Swirski, Victor Gomes, Olivier Flückiger, Darius Mercadier, Camillo Bruni (2023). Maglev - V8’s Fastest Optimizing JIT. V8 blog (v8.dev), 5 December 2023. v8.dev/blog/maglev (checked 2026-10-31)