Data Structures
11 min read

Arrays and Hash Maps: Engine Internals and Performance Reality

Theoretical complexity hides the real story. Arrays promise O(1) access but degrade to O(n) with sparse indices. Hash maps guarantee O(1) lookups until collisions chain into linear probes. Understanding V8’s internal representations—elements kinds, hidden classes, and deterministic hash tables—reveals when these data structures actually deliver their promised performance and when they fail.

Hash Maps (Map/Object)

Arrays

PACKED_SMI_ELEMENTS

fastest: all small ints

PACKED_DOUBLE_ELEMENTS

all floats

PACKED_ELEMENTS

mixed types

HOLEY_*_ELEMENTS

contains gaps

DICTIONARY_ELEMENTS

sparse: hash lookup

Monomorphic

single shape, inline cache

Polymorphic

2-4 shapes, slower

Megamorphic

many shapes, no optimization

Dictionary Mode

hash table fallback

⚠️ Transitions are ONE-WAY

Cannot revert to faster mode

V8's internal representation hierarchy. Both structures can irreversibly degrade from optimized to slow modes.

Memory layout determines real-world performance more than algorithmic complexity. Arrays exploit CPU cache lines through contiguous allocation—accessing adjacent elements costs nearly nothing when they share a 64-byte cache line. Hash maps trade memory for constant-time lookups, but load factors below 0.75 mean 25%+ waste, and collisions can chain into O(n) traversals.

V8 maintains multiple internal representations for both structures. Arrays transition through 21 “elements kinds”—from PACKED_SMI (fastest) to DICTIONARY (slowest)—and these transitions are permanent. Objects use “hidden classes” that enable monomorphic inline caches; adding properties dynamically forces costly shape transitions. Maps use Tyler Close’s deterministic hash table algorithm, maintaining insertion order through dual data structures (hash table for lookup, data table for iteration).

The critical insight: writing arr[1000] = x on an empty array, deleting object properties, or creating polymorphic call sites permanently degrades performance. Understanding these transitions matters more than memorizing Big O notation.

V8 categorizes arrays into 21 distinct “elements kinds” based on their contents. The engine optimizes operations differently for each kind, and transitions between kinds are irreversible.

The most common kinds, ordered from fastest to slowest:

Elements KindContentsAccess Pattern
PACKED_SMI_ELEMENTSSmall integers only (-2³¹ to 2³¹-1)Direct memory offset
PACKED_DOUBLE_ELEMENTSFloating-point numbersUnboxed 64-bit doubles
PACKED_ELEMENTSAny values (mixed)Boxed elements, type checks
HOLEY_SMI_ELEMENTSSmall ints with gapsHole check on every access
HOLEY_DOUBLE_ELEMENTSDoubles with gapsHole check + unboxing
HOLEY_ELEMENTSMixed with gapsHole check + type check
DICTIONARY_ELEMENTSSparse indicesHash table lookup
Elements kind transitions
2 collapsed lines
// Illustrating irreversible transitions
const arr = [1, 2, 3] // PACKED_SMI_ELEMENTS (optimal)
arr.push(4.5) // → PACKED_DOUBLE_ELEMENTS (still good)
arr.push("string") // → PACKED_ELEMENTS (boxed, slower)
arr[100] = "x" // → HOLEY_ELEMENTS (permanent hole)
// Cannot revert to PACKED_* even if you fill indices 4-99

When an array contains holes (missing indices), V8 must check every access to determine if the index contains a value or is empty. This check propagates through the prototype chain—a hole at arr[5] requires checking Array.prototype[5] and Object.prototype[5].

Hole check overhead
2 collapsed lines
// Demonstrating the prototype chain lookup cost
const arr = [1, , 3] // HOLEY_SMI_ELEMENTS
// Accessing arr[1] requires:
// 1. Check arr.hasOwnProperty(1) → false (hole)
// 2. Check Array.prototype.hasOwnProperty(1) → false
// 3. Check Object.prototype.hasOwnProperty(1) → false
// 4. Return undefined
// Contrast with packed array:
const packed = [1, 2, 3]
// Accessing packed[1]:
// 1. Direct memory read at (base + 1 * element_size)
// 2. Return value

Sparse arrays—those with large gaps between indices—switch to dictionary mode. V8’s heuristic triggers this when storing to an index would create too many holes relative to array capacity.

Triggering dictionary mode
2 collapsed lines
// Sparse assignment forces hash table storage
const arr = []
arr[0] = "first"
arr[1000000] = "sparse" // DICTIONARY_ELEMENTS
// Now arr[0] access requires:
// 1. Hash the key "0"
// 2. Probe the hash table
// 3. Return value if found
// Memory comparison (approximate):
// Dense arr[0..999999]: ~8MB (1M × 8 bytes)
// Sparse with 2 elements: ~140 bytes (hash table overhead)

Dictionary mode makes sense for genuinely sparse data. The trade-off: random access degrades from O(1) direct indexing to O(1) average hash lookup with potential O(n) worst-case on collision chains.

Objects in V8 use “hidden classes” (also called “shapes” or “maps” internally—not to be confused with the Map data structure). Hidden classes describe an object’s memory layout: which properties exist and at what offset.

When V8 first sees a property access like obj.x, it doesn’t know where x is stored. After the first access, it caches the property’s offset for that shape. Subsequent accesses to objects with the same shape use this “inline cache” for direct memory access.

Hidden class optimization
2 collapsed lines
// Demonstrating shape-based optimization
// All objects with same property order share a hidden class
function createPoint(x, y) {
const p = {}
p.x = x // Transition: {} → {x}
p.y = y // Transition: {x} → {x, y}
return p
}
const p1 = createPoint(1, 2)
const p2 = createPoint(3, 4)
// p1 and p2 share the same hidden class
function getX(point) {
return point.x // After first call: inline cached
}
getX(p1) // Misses cache, caches offset for shape {x,y}
getX(p2) // Hits cache! Direct memory read at cached offset

When a function receives objects with different shapes, V8’s inline cache becomes “polymorphic” (2-4 shapes) or “megamorphic” (many shapes). Megamorphic sites abandon caching entirely.

Polymorphism degradation
2 collapsed lines
// Demonstrating inline cache pollution
function getX(obj) {
return obj.x
}
// Creating objects with different shapes
const a = { x: 1 } // Shape: {x}
const b = { y: 2, x: 3 } // Shape: {y, x} — different!
const c = { x: 4, y: 5 } // Shape: {x, y} — also different!
const d = { z: 6, x: 7 } // Shape: {z, x}
const e = { x: 8, z: 9 } // Shape: {x, z}
// Calling with many shapes forces megamorphic
;[a, b, c, d, e].forEach(getX)
// getX is now megamorphic — falls back to dictionary lookup

The delete operator is particularly expensive. It forces the object into “slow mode” (dictionary properties) and cannot be undone.

Delete operator consequences
2 collapsed lines
// Why delete is slow
const obj = { a: 1, b: 2, c: 3 }
// obj uses fast properties (hidden class)
delete obj.b
// obj transitions to dictionary mode
// All subsequent property accesses use hash lookups
// Preferred alternative:
const obj2 = { a: 1, b: 2, c: 3 }
obj2.b = undefined // Maintains shape, just clears value
// obj2 keeps fast properties

JavaScript’s Map must iterate in insertion order (per ECMA-262). Standard hash tables don’t preserve insertion order, so V8 uses Tyler Close’s deterministic hash table algorithm.

V8’s Map implementation maintains two separate structures:

  1. Hash table: Array of bucket indices for O(1) key lookup
  2. Data table: Entries stored in insertion order

Data Table (insertion order)

Hash Table (buckets)

bucket[0] → 2

bucket[1] → 0

bucket[2] → 1

[0] key='first', val=1

[1] key='second', val=2

[2] key='third', val=3

Lookup: hash the key → find bucket → follow index to data table entry. Iteration: simply walk the data table sequentially (O(n) total, O(1) per element).

V8 computes hash codes differently by type:

Key TypeHash StrategyStorage
Small integers (Smis)Identity functionComputed on access
Heap numbersBit manipulation of valueComputed on access
StringsContent-based hashCached in string header
SymbolsContent-based hashCached in symbol header
ObjectsRandom seed-basedCached in object header

For objects, V8 generates a random hash code on first use and caches it. This prevents algorithmic complexity attacks while ensuring consistent hashing.

For small Maps (≤1022 entries), V8 stores the hash code in unused bits of the backing store’s length field. Since capacity maxes at 1022, only 10 bits are needed for length, leaving 21 bits for the hash code.

This optimization improved SixSpeed Map/Set benchmarks by ~500%.

For larger Maps or dictionary-mode objects, V8 allocates an extra word per object to store the hash code.

V8 uses separate chaining for collision resolution: entries with the same bucket form linked chains.

With pathological inputs, attackers can force all keys into the same bucket, degrading every operation to O(n).

CVE-2025-27209 (Node.js v24): V8’s rapidhash string hashing was vulnerable to collision attacks. Attackers could generate hash-colliding strings without knowing the hash seed, causing severe slowdowns.

Affected versions: Node.js v20.x, v22.x, v24.x Fixed in: v20.19.4, v22.17.1, v24.4.1

HashDoS impact
2 collapsed lines
// Illustrative example (do not use for attacks)
// If attacker controls keys and can generate collisions:
const map = new Map()
// Worst case: all keys hash to same bucket
// Each insertion: O(n) to traverse chain
// Total for n insertions: O(n²)
// Mitigation: rate limiting, input validation, updated runtime

V8 maintains a load factor (entries / buckets) around 0.5-0.75. When exceeded, the table rehashes:

  1. Allocate new table with ~2× capacity
  2. Recompute bucket indices for all entries
  3. Copy entries to new locations

This O(n) operation amortizes to O(1) per insertion over time, but causes latency spikes during resizing.

CriterionArrayObjectMap
Sequential dense data Optimal Wrong tool Wrong tool
Keyed lookups (strings) Good Best for frequent updates
Non-string keys Coerces to string Any key type
Insertion order Not guaranteed⚠️ Complex rules Guaranteed
Frequent add/delete⚠️ End operations only Delete is slow Designed for this
Iteration performance Cache-friendly⚠️ Property enumeration Sequential data table
Memory efficiency Best when dense⚠️ Hidden class overhead⚠️ Hash table overhead

Real-world V8 benchmarks (approximate, varies by workload):

OperationObjectMapWinner
Insertion4-5× fasterMap
Key lookup2-3× fasterMap
Has-key check~1×Tie
Deletion10-50× fasterMap
Iteration~1×~1×Tie (Map slightly better)

Objects win only when the shape is stable and properties are accessed via monomorphic call sites. For dynamic keyed collections, Map dominates.

Modern CPUs fetch memory in cache lines (typically 64 bytes). Accessing one element loads adjacent elements for free.

Cache-friendly vs cache-hostile
2 collapsed lines
// Demonstrating cache effects
// Cache-friendly: sequential access
const arr = new Array(1000000).fill(0)
for (let i = 0; i < arr.length; i++) {
arr[i] += 1 // Adjacent memory, cache hits
}
// Cache-hostile: random access
const indices = [...Array(1000000).keys()].sort(() => Math.random() - 0.5)
for (let i = 0; i < indices.length; i++) {
arr[indices[i]] += 1 // Random jumps, cache misses
}
// Sequential access is 3-10× faster than random access
// on the same data, due purely to cache effects

For numeric data, TypedArrays guarantee contiguous memory with fixed element sizes:

TypedArray memory layout
2 collapsed lines
// TypedArrays for performance-critical numeric work
const floats = new Float64Array(1000000)
// Guaranteed: 1000000 × 8 bytes = 8MB contiguous
// Regular array equivalent:
const regular = new Array(1000000).fill(0.0)
// V8 may optimize to contiguous doubles, or may not
// Adding a string element forces reallocation
// TypedArrays: fixed size, fixed type, maximum cache efficiency
// Regular arrays: flexible, but optimization is fragile
  1. Pre-allocate when size is known: new Array(n).fill(defaultValue) creates a packed array
  2. Avoid holes: Never skip indices or use delete arr[i]
  3. Keep types homogeneous: Mixing integers and floats transitions to PACKED_DOUBLE; adding strings transitions to PACKED_ELEMENTS
  4. Use TypedArrays for numeric data: Guarantees contiguous memory and type stability
  1. Initialize all properties in constructors: Establishes stable shape early
  2. Add properties in consistent order: Objects with properties added in same order share hidden classes
  3. Never use delete: Set to undefined or null instead
  4. Avoid dynamic property names in hot paths: Use Map instead
  1. Default choice for dynamic keyed collections: Designed for frequent add/remove
  2. Use when keys aren’t strings: Objects coerce keys; Maps preserve type
  3. Use when insertion order matters: Guaranteed iteration order
  4. Consider memory overhead for small collections: Object may be more efficient for <10 fixed keys

Big O notation describes algorithmic bounds, not real-world performance. V8’s internal representations—elements kinds for arrays, hidden classes for objects, deterministic hash tables for Maps—determine actual execution speed.

The actionable insights: keep arrays dense and homogeneous, maintain stable object shapes, use Map for dynamic collections. Understand that transitions to slower modes are permanent—a single sparse assignment or property deletion can degrade performance for the lifetime of that data structure.

  • Big O notation and amortized analysis
  • Basic understanding of hash tables and collision resolution
  • JavaScript object and array fundamentals
  • Elements kind: V8’s internal classification of array contents determining storage and access strategy
  • Hidden class / Shape: V8’s internal description of an object’s property layout enabling inline caching
  • Inline cache (IC): Cached property offset enabling direct memory access without property lookup
  • Monomorphic: Call site that always sees one shape; optimal for inline caching
  • Polymorphic: Call site seeing 2-4 shapes; uses polymorphic inline cache
  • Megamorphic: Call site seeing many shapes; abandons inline caching
  • Dictionary mode: Fallback to hash table storage when fast properties aren’t possible
  • Load factor: Ratio of entries to buckets in a hash table; affects collision probability
  • HashDoS: Denial-of-service attack exploiting hash collisions to degrade O(1) to O(n)
  • V8 arrays have 21 elements kinds; transitions from packed to holey/dictionary are permanent
  • V8 objects use hidden classes; maintaining consistent shapes enables inline caching
  • delete forces dictionary mode on objects—set to undefined instead
  • Maps use deterministic hash tables: hash table for lookup, data table for insertion-order iteration
  • Cache locality often matters more than algorithmic complexity—contiguous beats sparse
  • HashDoS attacks can degrade hash table operations from O(1) to O(n)

Read more

  • Previous

    Trees and Graphs: Traversals and Applications

    Programming & Patterns / Data Structures 17 min read

    Hierarchical and networked data structures that underpin file systems, databases, build tools, and routing. This guide covers tree variants (BST, AVL, Red-Black, B-trees, tries), graph representations (adjacency matrix vs list), traversal algorithms (DFS, BFS), and key algorithms (cycle detection, topological sort, shortest paths). Focus on design trade-offs and when to choose each structure.

  • Next

    K-Crystal Balls Problem: Jump Search Pattern

    Programming & Patterns / Algorithms Foundations 10 min read

    The K-crystal balls (or K-egg drop) problem demonstrates how constrained resources fundamentally change optimal search strategy. With unlimited test resources, binary search achieves O(log n). With exactly k resources that are consumed on failure, the optimal worst-case complexity becomes O(n^(1/k))—a jump search pattern where each resource enables one level of hierarchical partitioning.