Back to Browser Internals
Browser Internals
hard
senior

Why does deleting object properties hurt V8 optimization?

V8 builds hidden classes (maps) per object shape. `delete` mutates the shape and forces the object into slow dictionary mode, evicting it from inline-cache fast paths.

8 min read·~12 min to think through

JavaScript looks like a "dynamic bag of properties," but modern engines (V8 in Chrome/Node, JavaScriptCore in Safari, SpiderMonkey in Firefox) work hard under the hood to make object access feel like C-struct access. They do this with hidden classes (V8 calls them "maps," JSC "structures"). A hidden class is a C++ object describing an object's layout: which property names exist, in what order, and at what byte offset their slots live. Two JS objects created with the same property additions in the same order share one hidden class.

This sharing enables inline caches (ICs). When the JIT compiles obj.x, it emits machine code that essentially says: "if obj's hidden class pointer === the one I saw last time, read the value at byte offset N directly." That's a single load instruction — near-array-access speed. As long as call sites stay monomorphic (one hidden class) the engine inlines and devirtualizes aggressively.

delete obj.foo breaks the contract that the layout is stable. V8 has two responses depending on heuristics:

  1. Transition to a new hidden class that lacks foo. The object's previously cached property offsets may still be valid (V8 can shift the layout, but often it just marks the slot as a hole). However, every previously-monomorphic IC that saw the old hidden class now also has to handle the new one, escalating from monomorphic → polymorphic → megamorphic. Megamorphic call sites fall back to a dictionary lookup.
  2. Demote the entire object to "dictionary mode" (also called slow mode). The object becomes a hash map: lookups become hash → probe → load. This is orders of magnitude slower than the IC fast path, and once demoted, V8 almost never promotes it back.

V8 picks dictionary mode when: many properties have been deleted, properties have non-default attributes (writable: false, getters/setters), the object grows past a threshold, or you delete a property that isn't the last-added one. So even one badly-placed delete in a hot path can permanently de-optimize the object — and any function that reads from it.

Practical guidance:

  • Set to undefined (or null) instead of delete in hot objects. The slot remains; the value is just empty. ICs stay monomorphic.
  • Use Map when keys are truly dynamic. Map is designed for arbitrary key insertion and deletion; it does not maintain hidden classes for its keys and won't drag the rest of your code down.
  • Stabilize object shape in constructors: assign all properties up front, in the same order, even if some are null/undefined. Avoid conditional this.x = … blocks that produce different shapes per instance.
  • Avoid mixing types in the same property across instances — going from number to string triggers a transition too.
  • Measure, don't guess. Use node --allow-natives-syntax + %HaveSameMap(a, b) or --trace-maps to verify two objects share a hidden class. Chrome's V8 tracing flags and the Performance panel's Bottom-Up by Self time will show LoadIC_Megamorphic and StoreIC_Slow symbols when you've broken the fast path.

In application code this rarely matters — JSON-parsed objects, React state, etc. don't sit in tight loops. It matters most for tight numeric kernels, game engines, custom data structures, and library hot paths.

Code

js
// BAD — different shape per instance, slower property access
function User(name, email) {
  this.name = name;
  if (email) this.email = email;     // sometimes-present prop = shape variation
}

// GOOD — same shape every time
function User(name, email) {
  this.name = name;
  this.email = email ?? null;        // always set, hidden class is stable
}

// BAD — destroys the hidden class
delete user.email;

// GOOD — keeps the shape, marks the slot logically empty
user.email = null;
Hidden class friendliness

Follow-up questions

  • When IS dictionary mode actually fine?
  • How do you observe hidden-class transitions?
  • Does this matter for plain JSON parsing?

Common mistakes

  • Optimizing this for cold code — only matters in hot loops.
  • Assuming `Object.assign` preserves shape — it does, as long as keys are consistent.

Performance considerations

  • Run V8 with `--allow-natives-syntax` and use `%HasFastProperties(obj)` in benchmarks to verify.
  • `d8 --trace-maps` shows hidden class transitions in real time.

Edge cases

  • Sparse arrays trigger a similar slow path — `delete arr[i]` makes the array holey.
  • Frozen objects have a stable hidden class but lose monomorphic IC fast-paths after `Object.freeze` in some V8 versions.

Real-world examples

  • ORM-style models that conditionally `delete` fields before serializing tank list rendering performance.

Senior engineer discussion

Discuss inline cache megamorphism, the megamorphic stub cache, and how function megamorphism cascades from object shape variation.

Related questions