Why does deleting object properties affect 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.
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:
- 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. - 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(ornull) instead ofdeletein hot objects. The slot remains; the value is just empty. ICs stay monomorphic. - Use
Mapwhen keys are truly dynamic.Mapis 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 conditionalthis.x = …blocks that produce different shapes per instance. - Avoid mixing types in the same property across instances — going from
numbertostringtriggers a transition too. - Measure, don't guess. Use
node --allow-natives-syntax+%HaveSameMap(a, b)or--trace-mapsto verify two objects share a hidden class. Chrome's V8 tracing flags and the Performance panel's Bottom-Up by Self time will showLoadIC_MegamorphicandStoreIC_Slowsymbols 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
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.