Choosing a native collection
- Fixed schema / DTO / JSON: use
Object(avoidMap, noJSON.stringify) - Dynamic string-to-value dict: use
MaporObject.create(null)(avoid plain{}if keys are user input) - Non-string keys, insertion order,
.size: useMap(avoidObject) - Ordered list, index access, transform: use
Array(avoidSet, no indexes) - Unique values, fast membership: use
Set(avoidArray.includeson large/dynamic sets) - Attach data to objects without retaining them: use
WeakMap(avoidMap, leaks) - Track object presence without retaining: use
WeakSet(avoidSet, leaks) - Numeric / binary data, I/O, crypto: use TypedArray /
ArrayBuffer(avoidnumber[]for hot binary work) - Hot queue / deque / stack ends: do not rely on
Array.shift/unshift— use a custom ring or linked structure
Complexity and code characteristics
Choose a structure for the property you need to control — not only for speed.
Big-O (typical average case; n = collection size):
Objectfield access: O(1) for fixed shapes; dynamic/delete-heavy objects degrade toward dictionary modeArrayindex get/set,push/pop: O(1);includes/indexOf/splice/shift/unshift: O(n)Mapget/set/has/delete,Setadd/has/delete: amortized O(1); iteration: O(n)WeakMap/WeakSetget/set/has/delete: amortized O(1); no iteration- TypedArray index get/set: O(1); grow/copy: O(n)
- Prefer the structure whose hot operation is O(1); measure before optimizing rare paths
Readability and semantics:
- Let the type state the intent:
Set= unique membership;Map= keyed registry;Array= ordered sequence; plainObject= record/DTO - Prefer a named collection over encoding the same idea in ad-hoc flags, parallel arrays, or stringly keys
- Use array methods (
.map,.filter,.reduce) when they match the transform; use explicit loops when control flow or performance matters - Avoid over-abstracting: a small static
Arrayallowlist is clearer than aSetused once
Naming:
- Plural names for collections (
users,sessions); singular for entities (user) - Name by role, not by type:
seenIds(Set),socketToSession(Map) — notset1/mapData - Boolean membership reads as
has/is:if (seen.has(id)),blocked.has(user) - Include units in size names (
sizeBytes,timeoutMs) when the collection holds measured quantities
Stability and contracts:
- Treat inputs as immutable at API boundaries; copy or freeze when you must share
- Prefer stable iteration order when callers rely on it (
Map/Set/Arrayinsertion order) - Keep record shapes and array element kinds stable over time (see per-structure V8 hints)
- Document whether a returned collection is live (shared) or a snapshot
Testability:
- Prefer pure functions over collections: same inputs, same outputs; inject maps/sets as arguments when state is required
- Favor deterministic structures in tests: plain data over WeakMap (non-enumerable), over insertion-timed WeakRef behavior
- Assert on observable contents (
[...set],Object.fromEntries(map), length/size) rather than private fields - Small fixtures: build with literals/
Object.freezewhen the shape should not change under test
Encapsulation, safety, operability:
- Hide representation behind a small API (
add/has, getters) so internals can change without call-site churn - Use
Object.create(null)orMapfor untrusted keys (prototype-pollution safety) - Use WeakMap/WeakSet when lifetime must follow the key (leak control)
- Cap growth (max size, TTL, eviction) for long-lived Maps/Sets; unbounded collections are operability bugs
- Pick serializable structures (
Object/Array) at I/O boundaries; convertMap/Setexplicitly
Object
Use for records with a known, stable shape (config, entities, options).
const user = { id: 1, name: 'Ada', active: true };
// Pure dictionary (no prototype pollution via __proto__)
const dict = Object.create(null);
dict[key] = value;
Hints:
- Keep a stable shape (hidden class): same keys, types, and key order; init once in a factory/constructor
- Prefer fixed access (
obj.x) over dynamic keys (obj[key]) when the schema is known - Do not
deletefields; setnullfor refs,undefinedfor primitives - Objects that share a property should share the full shape — avoid polymorphic property reads
for...inis slow on hot paths; preferObject.keys/Object.entries
Array
Use for ordered sequences, stacks (push/pop), and small membership checks.
const ids = [10, 20, 30];
ids.push(40);
const last = ids.pop();
const ALLOWED = ['read', 'write'];
if (ALLOWED.includes(role)) {
/* ... */
}
Hints:
- Keep arrays dense and monomorphic (one element kind): SMI ints, doubles, or objects — do not mix
- Prefer SMI-friendly integers (31-bit signed) for indexes/counters (
PACKED_SMI_ELEMENTS); a float,NaN, or hole promotes the whole array - Avoid holey arrays (
new Array(n), deletes, skips); build withpushor fill sequentially - Avoid
shift()/unshift()in hot paths; use a queue or index cursors - Prefer
for/for...ofin hot loops; avoid array destructuring in hot assignments
Map
Use for dynamic key-value stores with any key type and predictable iteration.
const sessions = new Map();
sessions.set(socket, { userId: 42 });
sessions.set('fallback', null);
for (const [key, value] of sessions) {
// insertion order
}
sessions.delete(socket);
console.log(sessions.size);
Hints:
- Dynamic keys on
Mapavoid dictionary-mode / shape pollution on{} - Values read on hot paths should still use stable shapes
NaNis a valid single key; object keys compare by reference- No
JSON.stringify— useObject.fromEntries(map)only if keys are strings
Set
Use for unique values and fast membership.
const seen = new Set();
for (const id of incoming) {
if (seen.has(id)) continue;
seen.add(id);
process(id);
}
const unique = [...new Set(items)];
Hints:
- Uniqueness is by reference for objects, not deep value
- Insertion-order iteration; no index access
- Array/Set conversion allocates — do it at boundaries, not in hot loops
WeakMap / WeakSet
Weak collections hold keys weakly so entries can be GC'd when the key is otherwise unreachable. Keys must be objects or symbols. Not iterable; no .size.
const meta = new WeakMap();
const tracked = new WeakSet();
const attach = (obj, data) => {
meta.set(obj, data);
tracked.add(obj);
};
Hints:
- Prefer WeakMap side tables over adding ad-hoc fields to host objects
- Cannot enumerate — keep a strong list only if you must iterate
WeakRef/FinalizationRegistryare for lifetime hooks, not general storage
Typed arrays
Use for contiguous numeric/binary data: network frames, files, crypto, image/audio, WASM.
Uint8Array— bytes, I/O, hashingInt16Array/Uint16Array— PCM, compact IDsUint32Array/Int32Array— indexes, bitsets, WASM i32Float32Array/Float64Array— geometry, signals, tensorsBigInt64Array/BigUint64Array— 64-bit IDs, timestampsDataView— endian-aware protocol parsingArrayBuffer/SharedArrayBuffer— backing store; SAB for workers
const buf = new ArrayBuffer(16);
const bytes = new Uint8Array(buf);
const view = new DataView(buf);
view.setUint32(0, 0xdeadbeef, true);
bytes[4] = 0xff;
const frame = bytes.subarray(0, 8); // shared memory; `slice` copies
Hints:
- Fixed length — size ahead or allocate larger and copy to grow
- Prefer typed arrays over
number[]for tight numeric loops (stable element type) - Use
DataViewfor wire endianness; multi-byte typed views are platform-endian - Reuse/pool buffers in hot paths; Node
Bufferis aUint8Array— preferUint8Arrayin shared code unless you need Buffer APIs
Conventions
- Keep hot code monomorphic: stable argument count, types, return types, and object shapes
- Prefer SMI integers for indexes/sizes; do not mix ints and floats in one variable
- Reduce GC pressure: reuse arrays, objects, and buffers when safe
- Move reusable callbacks outside loops; keep
try/catchand spread out of hot loops