Dictionary-Mode Objects and the delete Operator
A lookup table implemented as a plain object gets slower and heavier as it grows, a hot object becomes sluggish after a delete, and heap snapshots show objects whose property storage is a large hash table rather than a compact array. This guide from Object Shapes, Strings and Collection Memory Costs, part of JavaScript Memory Fundamentals & Runtime Mechanics, explains V8’s two object representations, what pushes an object into dictionary mode, what that costs, and when to reach for a Map instead.
| Symptom | Root Cause | Immediate Action | Measurable Impact |
|---|---|---|---|
Hot object slower after delete obj.prop |
Deleting a non-last property switches the object to dictionary mode | Set the property to undefined or restructure |
Object stays in fast mode; inline caches remain effective |
| Object used as a dynamic key/value store grows heavy | Many added/removed keys force dictionary mode | Use a Map |
Predictable memory and fast insert/delete |
Heap snapshot shows large properties hash tables |
Objects in dictionary mode store properties in a hash table | Identify the owning objects and their usage pattern | Targets only the objects that matter |
| Records with the same fields have different memory costs | Some instances went through delete or late additions |
Initialise all fields in the constructor | Consistent shapes and sizes |
| Performance cliff after thousands of properties | Fast mode has limits on the number of in-object/fast properties | Use Map for large, dynamic keyed data |
No cliff; steady performance |
Root Cause: Two Representations, Chosen by How You Use the Object
V8 represents most objects in fast mode: the object points to a hidden class (a “map” in V8 terms) that describes its layout — which properties exist, in what order, and at what offset each value is stored. Property values live in slots inside the object or in a compact backing array. Code that accesses obj.x can cache the offset for a given hidden class in an inline cache, making property access about as fast as a struct field. Objects created the same way share the same hidden class, which is what makes this efficient, as described in how hidden classes and inline caches affect memory.
Some usage patterns do not fit that model. When you delete a property that is not the most recently added one, V8 cannot simply revert to a previous hidden class, so it typically converts the object to dictionary mode (also called slow mode): properties are stored in a per-object hash table keyed by name, and the hidden class no longer describes the layout. Objects that have many properties added dynamically — using an object as a key/value map with thousands of distinct keys — are also moved to dictionary mode, because creating a new hidden class for every key would be wasteful.
Dictionary mode is not “bad”; it is the right representation for a genuine key/value store, and it avoids a hidden-class explosion. But it has costs where you did not intend it. Each dictionary-mode object carries its own hash table with spare capacity, so a small record in dictionary mode can use several times the memory of the same record in fast mode. Property access must hash the key rather than read a cached offset, and inline caches at call sites that see such objects become megamorphic or generic, slowing unrelated code that shares those call sites. Objects rarely return to fast mode on their own.
For data that is truly a dynamic map, use a Map: it is designed for frequent insertion and deletion, keeps insertion order, accepts any key type, and has predictable memory, as compared in Map vs Object vs Array memory overhead. Keep plain objects for records with a fixed set of fields.
Step-by-Step Fix
- Find objects used as dynamic maps. Search for objects that receive computed keys (
obj[key] = value) anddelete obj[key]at runtime, especially module-level lookup tables and caches. Verification: you have a list of objects whose key set changes during execution. - Confirm their representation in Node. Run a diagnostic with
node --allow-natives-syntaxand%HasFastProperties(obj)on a representative instance. Verification: dynamic stores reportfalse(dictionary mode); records that should be fast reporttrue. - Replace dynamic stores with
Map. Convert lookup tables, registries and caches with changing keys toMap(get,set,delete,has). Verification: code no longer usesdeleteon those objects, and iteration usesmap.entries(). - Stop deleting fields from records. For fixed-shape records, set a field to
undefined(ornull) instead of deleting it, and declare every field in the constructor or factory. Verification:%HasFastPropertiesstaystruefor records after updates. - Measure memory and speed. Compare heap snapshots (shallow and retained size of the objects) and a micro-benchmark of the hot path before and after. Verification: per-record memory falls and hot-path timing improves or stays equal.
- Keep shapes consistent across instances. Create records through one constructor or factory that assigns fields in the same order. Verification: snapshots show one shared hidden class for the record type rather than many variants.
Command and Code Reference
Use case: check an object’s representation in Node.js (diagnostics only).
// shapes.js — node --allow-natives-syntax shapes.js
const user = { id: 42, name: 'Ada', email: '[email protected]' };
console.log('fast before delete:', %HasFastProperties(user)); // true
delete user.name; // not the last-added property
console.log('fast after delete:', %HasFastProperties(user)); // typically false
const user2 = { id: 43, name: 'Grace', email: '[email protected]' };
user2.name = undefined; // keep the field, clear the value
console.log('fast after undefined:', %HasFastProperties(user2)); // true
Use case: replace an object-as-map with a Map.
// Before: dynamic keys and deletes push the object into dictionary mode
const sessions = {};
function open(id, data) { sessions[id] = data; }
function close(id) { delete sessions[id]; }
// After: a Map is built for exactly this access pattern
const sessionMap = new Map();
function openSession(id, data) { sessionMap.set(id, data); }
function closeSession(id) { sessionMap.delete(id); }
Use case: records with a stable shape.
// Every field declared up front, in the same order, for every instance
class Row {
constructor(id, label) {
this.id = id;
this.label = label;
this.selected = false; // declared even if usually false
this.error = null; // declared instead of added later
}
}
Verification and Regression Prevention
Verify with two measurements: a heap snapshot comparing the retained size of the affected collection before and after (dictionary-mode records and object-as-map stores typically shrink noticeably), and a benchmark of the hot path that reads those objects. Also check that the hidden-class count for your record type stays small — in a snapshot, records of one type should share one map rather than dozens of variants.
Prevent regressions with a lint rule against delete on non-Map objects in performance-sensitive modules (for example ESLint’s no-dynamic-delete from typescript-eslint, or a custom rule), and a convention that dynamic key/value data uses Map. Keep %HasFastProperties checks out of production code; they require a special flag and are for investigation only.
Edge Cases and Gotchas
Deleting the last-added property
Deleting the most recently added property can sometimes let V8 transition back to the previous hidden class without dictionary mode. Relying on that is fragile; if you need to remove fields, prefer undefined or a Map.
Object spread and rest create new objects
const { name, ...rest } = user creates a new object without name — in fast mode — rather than mutating user. For occasional removal of fields from a copy, this is a clean alternative to delete.
Integer-like keys are elements
Keys such as "42" are stored as indexed elements, not named properties, and follow array-like storage rules, including their own sparse “dictionary elements” mode for very sparse indices. Objects keyed by numeric IDs behave differently from objects keyed by names; a Map avoids both sets of surprises.
Prototype objects
V8 treats objects used as prototypes specially and may keep them in dictionary mode during setup, optimising them later. That is internal behaviour; do not try to influence it.
Frequently Asked Questions
Does the delete operator cause memory leaks?
No — deleted values become collectable. But delete on a non-last property usually converts the object to dictionary mode, which makes that object larger and slower to access. For records, set fields to undefined; for dynamic key sets, use a Map.
What is dictionary mode in V8?
It is the slower object representation in which properties are stored in a per-object hash table instead of at fixed offsets described by a shared hidden class. V8 uses it for objects with many dynamically added or removed properties.
Is Map always better than an object?
For dynamic key/value data with frequent insertions and deletions, usually yes: predictable performance, any key type and no hidden-class churn. For records with a fixed set of named fields, plain objects in fast mode are smaller and faster.
How can I tell if an object is in dictionary mode?
In Node, run with --allow-natives-syntax and call %HasFastProperties(obj) in a diagnostic script. In heap snapshots, dictionary-mode objects show a properties backing store that is a hash table, and they do not share a common map with similar objects.
Related
- Object Shapes, Strings and Collection Memory Costs — the parent topic
- How Hidden Classes and Inline Caches Affect Memory — the fast-mode side of the story
- Array Elements Kinds and Holey Array Memory — the equivalent transitions for array elements
- JavaScript Memory Fundamentals & Runtime Mechanics — the section overview