Packed Arrays and Hidden Classes Make Your Constants Huge or Tiny
V8 Arrays and Objects Internals
V8 uses hidden classes, packed vs holey array representations, and SMI tagging to optimise property access — understanding these changes your algorithmic constants without changing Big-O.
What you'll learn
- Explain what hidden classes are and when V8 creates a new one
- Distinguish packed, holey, and dictionary-mode arrays in V8
- Write code that stays in V8's fast path for arrays and objects
Two algorithms can share the same Big-O yet run 10× apart on real data. The difference is often not the algorithm — it is the V8 representation chosen for the data. V8 makes aggressive assumptions about objects and arrays. Break those assumptions and you pay a penalty in hidden overhead.
Hidden Classes
When you create an object literal, V8 internally assigns it a hidden class (also called a “shape” or “map”). The hidden class encodes which properties exist and in what order. V8 can then use a compact struct layout and fast property lookups.
// V8 assigns one hidden class here
const point = { x: 0, y: 0 };
// Adding a property AFTER creation creates a new hidden class
point.z = 0; // shape transition — V8 must update the object's class Every shape transition is cheap alone but causes megamorphic deopt if you add properties in varying orders across many objects. Keep object shapes stable:
// Good — all objects share one hidden class
function makePoint(x, y) {
return { x, y };
}
// Bad — the object starts with one shape, then transitions
function makePointBad(x, y) {
const p = {};
p.x = x; // new hidden class
p.y = y; // another transition
return p;
} SMIs — Small Integers
V8 uses a tagged-pointer representation. Integers that fit in 31 bits on 32-bit
builds (or 32 bits on 64-bit builds) are stored as SMIs (Small Integers)
— no heap allocation, no boxing. Floating-point numbers and large integers
are heap-allocated HeapNumber objects.
const a = 42; // SMI — no allocation
const b = 42.5; // HeapNumber — heap allocation
const c = 2 ** 32; // too large for SMI — HeapNumber A tight numeric loop over an array of SMIs runs significantly faster than one over floats because V8 avoids pointer chasing.
Packed vs Holey Arrays
V8 tracks whether an array has gaps. A packed array has contiguous elements
with no undefined holes. A holey array has at least one hole.
const packed = [1, 2, 3]; // PACKED_SMI_ELEMENTS
const holey = [1, , 3]; // HOLEY_SMI_ELEMENTS — the gap is a hole
const alsoHoley = new Array(3); // HOLEY_ELEMENTS — pre-sized, uninitialized
// Deleting an element punches a hole
const arr = [1, 2, 3];
delete arr[1]; // now holey — avoid delete on arrays Packed arrays allow V8 to skip per-element prototype chain checks. Holey arrays
must check each slot against undefined and the prototype. The difference is
measurable in tight loops.
V8 element kinds, from fastest to slowest:
| Element Kind | Description | Trigger |
|---|---|---|
PACKED_SMI_ELEMENTS | Dense, all small integers | [1, 2, 3] |
PACKED_DOUBLE_ELEMENTS | Dense, all doubles | [1.1, 2.2] |
PACKED_ELEMENTS | Dense, mixed types | [1, "a"] |
HOLEY_SMI_ELEMENTS | Gaps, integers | [1,,3] |
HOLEY_DOUBLE_ELEMENTS | Gaps, doubles | [1.5,,3.5] |
HOLEY_ELEMENTS | Gaps, mixed | [1,,"a"] |
DICTIONARY_ELEMENTS | Sparse hash map | Very large indices or delete |
Transitions go downward — once holey, always holey for that array instance.
Dictionary-Mode Objects and Arrays
If an array becomes very sparse (e.g., arr[999999] = 1 on an empty array) or
an object accumulates too many properties at runtime, V8 switches to
dictionary mode — a hash map internally. Hash map access is slower than
struct-style property access.
// Triggers dictionary mode on the array
const sparse = [];
sparse[0] = 1;
sparse[9999] = 2; // huge index gap — V8 may use a hash map internally
// Use a Map instead for sparse key-value storage
const map = new Map();
map.set(0, 1);
map.set(9999, 2); // intentionally a hash map — no surprise transition Up Next
How V8 handles values internally connects directly to the deeper question of value vs reference semantics — how JS copies, compares, and mutates data.
Primitives vs References →