JavaScript Array Packing: Memory and Performance

Array packing in modern JavaScript engines determines whether an array’s elements are stored contiguously without gaps (packed) or sparsely with missing indices (holey). This internal distinction dictates the underlying C++ memory structure the engine allocates, directly influencing CPU cache locality, JIT compiler optimizations, and runtime execution speed.

How JavaScript Engines Represent Arrays

Although JavaScript provides a single, high-level Array object, engines like V8 (Chrome, Node.js), SpiderMonkey (Firefox), and JavaScriptCore (Safari) do not treat all arrays equally. Instead, they track internal element types (often referred to as “ElementsKinds” in V8) based on two primary factors:

  1. Packing status: Whether the array is Packed (dense) or Holey (sparse).
  2. Value types: Whether the array holds integers (SMI or Small Integers), floating-point numbers (Double), or mixed objects/references (Elements).

An array created as [1, 2, 3] is classified as a packed integer array. If an index is skipped, such as assigning arr[100] = 5, the engine transitions the array into a holey representation.

Memory Layout Differences

The packing state changes how the engine allocates and accesses memory in the backing store:

1. Packed Arrays (Flat Memory Buffers)

Packed arrays allocate a contiguous, flat block of memory. Each element directly follows the previous one: - Direct Pointer Arithmetic: Accessing index i requires only an offset calculation: base_pointer + (i * element_size). - Zero Overhead for Gaps: No extra metadata or sentinel values are required to track whether an index contains an initialized value.

2. Holey Arrays (Sparse Representations and Fallbacks)

When an array contains holes (e.g., let a = new Array(3) or delete a[1]): - Sentinel Checks: The backing memory must store special “hole” markers (like a unique internal uninitialized value) to differentiate between an undefined element (arr[0] = undefined) and an unassigned slot. - Dictionary Mode: If an array becomes excessively sparse (e.g., arr[1000000] = 1), engines abandon contiguous memory entirely and transition the array into a hash table (dictionary mode), dramatically increasing memory overhead and access latency.

Performance Implications

Array packing has a direct, measurable effect on JavaScript performance across three key areas:

1. CPU Cache Locality

Packed arrays maximize CPU L1/L2 cache efficiency. Because the elements reside in contiguous memory addresses, loading one element pre-fetches neighboring elements into the CPU cache line. Holey arrays, especially those converted to dictionary mode, scatter elements across memory, causing frequent CPU cache misses.

2. Prototype Chain Lookups

In JavaScript, reading a missing index from an array requires the runtime to traverse the prototype chain (Array.prototype, Object.prototype) to check if a property with that index exists higher up. - Packed Array: The engine knows every index within bounds contains a value, bypassing prototype chain lookups entirely. - Holey Array: Accessing a hole forces the runtime to fall back to the prototype chain to ensure no matching property has been defined on Array.prototype.

3. JIT Inlining and Type Transitions

Just-In-Time (JIT) compilers, like V8’s TurboFan, generate highly optimized machine code for packed arrays. They can eliminate bounds checks and omit type guards.

Transitions between array representations are unidirectional: once an array transitions from packed to holey, or from integer to double, it almost never transitions back to a more optimized state. Operations on that array will remain de-optimized for the remainder of its lifecycle.

Code Patterns to Maintain Packed Performance

To preserve contiguous memory layouts and maximize engine optimization: