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

Fast properties, slow properties, and slack tracking

Three storage modes: in-object properties (inline in the body, single load), out-of-object fast properties (PropertyArray, +1 dereference), and dictionary mode (NameDictionary hashmap, ~50-100 cycles, set by deletes or too many props).

JSE Middle ◷ 14 min
Level
FoundationsJuniorMiddleSenior

You construct a class with three fields, but the very first instances V8 allocates are bigger than they need to be — padded with empty slots. Then, after a few hundred instances, V8 quietly shrinks the class and frees the padding. This is slack tracking, and if you never knew it existed you have been mis-reading heap snapshots taken during warm-up. Property storage in V8 has three modes and one self-tuning allocator, and which one your object lands in decides whether a read is one instruction or a hash probe.

Three storage modes, three speeds

Why does storage location matter so much? Because a 50-100x cost difference between modes is the kind of cliff that shows up as a flat-line CPU flame chart but disappears in micro-allocations — you only find it by knowing what to look for before profiling.

The DescriptorArray from lesson 01 tells V8 where a field lives. There are three possible answers, in descending speed order:

  1. In-object properties. Stored inline in the object’s own memory block, right after the header. Reading one is a single load at a constant offset — the fastest possible. The number of in-object slots is fixed when the Map is created.
  2. Out-of-object (“normal”) fast properties. When an object outgrows its in-object slots, further properties spill into a separate PropertyArray that the object points to. These are still fast (fixed-offset, IC-friendly) but cost one extra pointer dereference: load the PropertyArray pointer, then load the slot. The DescriptorArray records a negative-ish index meaning “this field is in the PropertyArray at index N.”
  3. Dictionary mode (slow properties). When an object becomes too dynamic — too many properties, or any delete — V8 abandons the fixed-offset layout entirely and switches the object to a NameDictionary: a per-object hash table from property name to value+attributes. Every access is now a generic hash lookup, roughly 50–100 cycles versus ~1 for an in-object load, and the inline cache cannot help. %HasFastProperties(obj) returns false.
// d8 --allow-natives-syntax
const a = { p: 1, q: 2, r: 3 };
%HasFastProperties(a);   // true  — in-object, fixed offsets
delete a.q;
%HasFastProperties(a);   // false — now a NameDictionary, permanently slow

Slack tracking: V8 right-sizes your objects

Here is the self-tuning part the overview skipped. When you define a constructor or class, V8 does not yet know how many properties instances will end up with — this.x = ...; this.y = ... might be followed by more assignments in methods called later. Allocating exactly two in-object slots up front would force a PropertyArray spill the moment a third field appears.

So V8 over-allocates: the initial Map for the constructor reserves extra in-object slots (slack) — more than the constructor’s body declares. As instances are created and run, V8 watches the maximum number of in-object properties they actually use. After enough instances have been allocated (a fixed construction-count threshold), it completes slack tracking: it trims the unused slots, shrinks the instance size in the Map, and finalizes the Map. From then on, every new instance is allocated at the trimmed, exact size.

The consequences for a senior:

  • Warm-up instances are larger than steady-state instances. A heap snapshot taken before slack tracking completes overstates per-object size. Measure after warm-up.
  • The Map is not “final” immediately. Code that depends on a stabilized shape (and TurboFan’s specialization) only gets it after slack tracking finishes. Microbenchmarks that allocate a handful of objects can see a different shape than production at scale.
  • Adding properties outside the constructor after finalization can no longer fit the in-object area and spills to a PropertyArray (or, if dynamic enough, dictionary mode).

What pushes an object into dictionary mode

Dictionary mode is the cliff. You fall off it via:

  • delete obj.prop on a fast object — the single most common cause. V8 cannot leave a hole in a fixed-offset layout, so it converts the whole object to a NameDictionary. This is permanent: the object never climbs back to fast properties.
  • Too many properties added dynamically (the threshold is large, on the order of many hundreds to ~1000+, and depends on the version), so it is rarely the real culprit — delete is.
  • Adding properties in a way that defeats the transition tree — extreme shape churn can cause V8 to give up and switch to dictionary mode to stop allocating Maps.

Note const folding interacts here: when a field is const (only one value ever assigned across instances), V8 can store the value in the Map (in the descriptor) rather than in the object, so reads become “load the constant from the Map” — but the first divergent assignment loses constness (lesson 02) and the value moves into a real field. Dictionary mode abandons all of this.

Property storage costs (V8, 64-bit)
In-object read
1 load (~1 cycle)
PropertyArray read
2 loads (+1 dereference)
Dictionary-mode read
~50-100 cycles, hash probe
What triggers dictionary mode
delete, or extreme dynamism
Recovery from dictionary mode
none — needs a fresh object
Slack tracking completes after
a fixed construction-count threshold
Detect mode
%HasFastProperties(obj) in d8
Quiz

A property read compiles to 'load PropertyArray pointer, then load slot 2 of that array'. Which storage mode is this property in?

Quiz

A heap snapshot taken after allocating 10 instances of a class shows each instance is larger than a snapshot taken after allocating 10000. Why?

Order the steps

Order the slack-tracking lifecycle for a constructor's instances.

  1. 1 The initial Map reserves extra in-object slots beyond what the constructor body declares
  2. 2 Instances are allocated; V8 records the maximum in-object properties actually used
  3. 3 A fixed construction-count threshold of instances is reached
  4. 4 V8 trims the unused in-object slots and shrinks the instance size
  5. 5 The Map is finalized; every later instance is allocated at the trimmed exact size
Common mistake

A common self-inflicted dictionary-mode wound: using a plain object as a hash map with user-controlled keys (cache[userId] = entry) and then delete cache[userId] to evict. The first delete converts cache to dictionary mode — which is actually correct for a real map, but you also lose any IC benefit on it and pay hash-probe cost on every access. If the object is conceptually a dictionary, use a real Map from the start: it is built for arbitrary keys and deletions, never churns hidden classes, and has predictable performance.

Recall before you leave
  1. 01
    Describe the three property-storage modes and the cost of a read in each.
  2. 02
    What is slack tracking and why must you measure object size after warm-up?
  3. 03
    Why is `delete` worse than assigning null, in storage terms?
Recap

V8 stores an object’s named properties in one of three modes. In-object properties sit inline in the object’s body and read in a single load at a constant offset. When an object outgrows its in-object slots, extra properties spill into an out-of-object PropertyArray, costing one additional pointer dereference per read but staying fixed-offset and IC-friendly. Dictionary mode is the cliff: triggered chiefly by delete (and by extreme dynamism), it swaps the fixed layout for a per-object NameDictionary hash table, making every access a ~50-100 cycle hash probe with no inline-cache help, and it is permanent — only a fresh object recovers fast properties. Slack tracking is V8’s self-tuning allocator for fast objects: the constructor’s initial Map reserves extra in-object slots, V8 observes how many instances actually use as they are created, and after a fixed construction-count threshold it trims the slack, shrinks the instance size, and finalizes the Map — so per-object memory only settles after warm-up. Detect the mode with %HasFastProperties(obj), prefer null over delete, and reach for a real Map when keys are genuinely dynamic. Now when you see %HasFastProperties return false on a hot object, or a heap snapshot where object sizes shrink after warm-up, you know exactly which layer to investigate first.

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.