open atlas
↑ Back to track
JavaScript Engine internals JSE · 03 · 01

What a Map really is: opening the hidden class

A V8 Map is a fixed-size HeapObject describing one object layout: instance size, a DescriptorArray (name → field index, representation, constness, attributes), elements kind, a back-pointer, and transitions. Objects with identical layout+history share it

JSE Middle ◷ 14 min
Level
FoundationsJuniorMiddleSenior

In browser/03-v8-internals you learned that two objects with the same property-addition order “share a hidden class,” and that this is what makes property reads fast. Fine — but what is that thing? It is not a tag or a flag. It is a real, allocated C++ object sitting on the heap, with a precise layout you can inspect in d8. This lesson opens the box and names every field inside a Map.

The object is almost empty; the Map holds the knowledge

You saw the overview in browser/03-v8-internals/03-hidden-classes: objects sharing a hidden class share the fast inline-cache path. Here we open that box. The single most important structural fact in V8 is this: a JavaScript object carries almost no metadata of its own. A plain object’s body in memory is just a small contiguous block:

[ map pointer ][ properties pointer ][ elements pointer ][ in-object slot 0 ][ slot 1 ] ...

The first word is a pointer to a Map (V8’s name for the hidden class; Shape in SpiderMonkey, Structure in JavaScriptCore). Everything that tells V8 what this object is — which property names exist, where each one lives, what type each holds — is not in the object. It is in the Map. The object only stores the values, packed into anonymous slots at fixed byte offsets. The names and offsets live one indirection away, in the shared Map.

That is the whole trick. Because the layout knowledge is factored out into the Map, a property read p.x can compile to: load the map pointer, compare it to the map the inline cache expects, and on a match load the slot at a hard-coded offset. No name comparison, no hash lookup — the name x was resolved to an offset once, at the Map level, and reused for every object that shares that Map.

Inside the DescriptorArray

Before you can read the DescriptorArray correctly, ask yourself: what exactly do you need to know per property to compile a property access to a single load? The answer lives here.

The heart of the Map is the DescriptorArray — the table that turns a property name into everything V8 needs to read or write it. One entry per named (string-keyed) own property, in declaration order. Each descriptor records:

  • The key — the (usually interned) property name, e.g. "x". Interned names compare by pointer, so finding a descriptor is a pointer scan, not a string compare.
  • The field indexwhich slot holds the value, and whether that slot is in-object (inline in the object body) or out-of-object (in a separate PropertyArray). We open this fully in lesson 03.
  • The representation — what kind of value the field is statically known to hold: Smi (small integer, unboxed), Double (an unboxed 64-bit float stored in a “mutable HeapNumber” slot), HeapObject (a pointer to any heap object), or Tagged (anything — the most general). Narrower representations let TurboFan skip boxing and type checks.
  • Constnessconst if the field has only ever been assigned one value across all instances of this Map (so the compiler may inline the value), or mutable once a second value appears.
  • Attributes — the property’s writable / enumerable / configurable bits, plus whether it is a plain data field, a const data slot folded into the descriptor, or an accessor (getter/setter) pair.

Together these five fields mean that once the Map is shared, V8 knows exactly how to read or write any property without touching the object itself — if you remove any one of them (say, representation), the compiler can no longer skip boxing or choose the right instruction; without field index, it cannot find the slot at all.

// Logical contents of the DescriptorArray for an object built as { x: 1, y: 2.5 }:
// index 0:  key="x"  field#=0  rep=Smi      const=true  attrs=W,E,C  storage=in-object
// index 1:  key="y"  field#=1  rep=Double   const=true  attrs=W,E,C  storage=in-object

Crucially, two different Maps can share the same DescriptorArray — a parent Map and a child Map that only added one property at the end can point at the same backing descriptor array, with the child declaring it owns one more descriptor than the parent. This is why building a tree of related shapes is cheap: most of the descriptor data is reused, not copied.

The back-pointer and transitions: a Map knows its neighbours

A Map is a node in a tree, and it stores the edges:

  • The back-pointer points to the Map this one was derived from — the layout before the last property was added. V8 walks back-pointers to find a common ancestor when reconciling shapes, and (as we will see in lesson 02) to migrate deprecated layouts.
  • The transitions pointer points forward, into a TransitionArray keyed by {property name, attributes}, to the child Maps reached by adding the next property. Adding "y" to a Map-for-{x} follows (or creates) the "y" transition edge to the Map-for-{x,y}.

So a Map is simultaneously a description of the current layout and a router to neighbouring layouts. That dual role is what makes shape transitions O(1) amortised and what makes the whole shape graph shareable across the isolate.

What lives where (V8, 64-bit)
Object header before in-object slots
map + properties + elements = 3 words
Map size (fixed)
~10 pointer-sized fields
Maps per layout (shared)
1 — all matching objects share it
Field representations
Smi, Double, HeapObject, Tagged
Property read on map hit
load map, compare, load at offset (~2-3 instrs)
Inspect a Map in d8
%DebugPrint(obj) at --allow-natives-syntax

You can see all of this directly. In d8 --allow-natives-syntax, %DebugPrint(obj) prints the object’s Map address, its DescriptorArray with each property’s representation and field index, the elements kind, and the prototype. Two objects that “share a hidden class” print the same Map address; two that diverged print different ones.

Quiz

For a plain object `{x:1, y:2}`, where does the information 'property x lives at field index 0' physically live?

Quiz

You allocate 100000 objects via `new Point(x, y)`, all with the same two fields in the same order. How many Map objects exist for them?

Order the steps

Order the steps V8 takes to read `p.x` on an object whose Map matches the inline cache's expectation.

  1. 1 Load the map pointer from the object's header
  2. 2 Compare it to the Map the inline cache recorded for this site
  3. 3 On a match, take the cached field index resolved earlier from the DescriptorArray
  4. 4 Load the value directly from that fixed slot — no name compare, no hash lookup
Why this works

Why factor layout out of the object at all? Because most objects of a given type are structurally identical — millions of DOM nodes, AST nodes, or React fibers share a handful of shapes. Storing each one’s name-to-offset table inline would multiply memory by the field count and make every read a search. Sharing one Map per layout turns “describe this object” from per-instance data into a single, cacheable pointer — which is exactly what an inline cache compares.

Recall before you leave
  1. 01
    List the fields stored in a V8 Map and say which are inline versus pointers to other heap objects.
  2. 02
    What is in one DescriptorArray entry, and why does it make reads fast?
  3. 03
    Why does allocating a million objects of the same shape not allocate a million Maps, and how would you confirm it?
Recap

A V8 Map (the hidden class; Shape in SpiderMonkey, Structure in JavaScriptCore) is a real, fixed-size HeapObject that fully describes one object layout, and it is shared by every object built with the same layout and history. Its inline fields are the instance size, the instance type and bit-fields, the count of own descriptors, and the elements kind; it points to a DescriptorArray, a prototype, a back-pointer to the parent Map, and a TransitionArray of children. The DescriptorArray maps each property name to a field index, a representation (Smi/Double/HeapObject/Tagged), a constness flag, and attributes. The object instance itself is almost empty — just the map pointer plus anonymous value slots at fixed offsets. That separation is the engine for fast reads: resolve the name to an offset once in the Map, then read p.x as “load map pointer, compare to the inline cache’s Map, load the slot.” The next lessons follow this structure forward (transitions, deprecation, and migration) and downward (how field storage and access actually work). Now when you see a performance regression or a deopt trace, your first question is: how many distinct Maps does this hot site see, and what does each Map’s DescriptorArray say about field representation?

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 5 done
Connected lessons
appears again in184

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

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.