Viscosity
Review
Part V · Inside real JavaScript engines · Day 16

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.

About 18 minutes, including the exercises and quiz
By the end you will be able to
  • 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

Intuition

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.

Definition

A shape (in V8, a map; also a hidden class) describes an object’s layout: its named properties, their order, and where each value lives. Objects with the same properties added in the same order share one shape[1]. The shape also records each field’s representation (chapter 22)[4].

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:

Step throughA transition tree grows
const a = {};
a.x = 1;
a.y = 2;
const b = {};
b.x = 5;
b.y = 6;
const c = {};
c.y = 2;
c.x = 1;
A tree of shapes. The root is the empty shape. One branch adds x then y; another adds y then x. Objects a, b and c point at the shape they currently have.S0{ }a
An empty object starts with the root shape: no properties. Every {} made this way shares it.
1/7

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]...>
Definition

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.

Try itOrder is part of an object
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.

Definition

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].

Pitfall

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.

Under the hood

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

ExerciseSimulate a transition tree

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);
}
Extra practiceDetect property-order mismatches

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. delete triggers it here; reads get slower.
  • Build objects of one kind the same way, in the same order, and don’t delete their fields.

Next: how V8 uses shapes to make o.x fast at one particular line of code.

Check your understanding

0/6
  1. 1.

    What does a V8 object store itself, and what does its map store?

  2. 2.

    const a = {}; a.x = 1; a.y = 2; and then const b = {}; b.x = 3; b.y = 4;. How many new shapes does b create?

  3. 3.

    Why does the order of property additions matter to V8?

  4. 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. 5.

    What is dictionary mode?

  6. 6.

    You have 100,000 particle objects and want to mark some as dead. Which keeps them fast?

References

  1. [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. [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. [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. [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. [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)