TRT
Sign in
easyFoundationsconcept

Explain V8 Hidden Classes (Shapes/Maps) and Inline Caches (Monomorphic, Polymorphic, Megamorphic). Why doesn't a TypeScript `interface` automatically.

Walk through in-object property offsets, Map transition trees, the 4-shape polymorphic threshold, `delete` dictionary degradation, and why TypeScript type erasure leaves runtime property order in control.

TRT practice prompt — not a verified question from a named employer.

The Runtime Theory Team2 min read

A strong answer

Because JavaScript is dynamically typed, V8 avoids slow hash-table lookups on event.latencyMs by attaching a pointer to a Hidden Class (called a Map or Shape) at byte offset 0 of every heap object. Each time you add a property, V8 follows a transition edge in a Hidden Class tree (Map0 -> Map1(id @ +24) -> Map2(latencyMs @ +32)). At every property read site, V8's JIT (Ignition/Maglev/TurboFan) installs an Inline Cache (IC): (1) Monomorphic IC (1 Shape seen): compares obj.map == ExpectedMap in 1 CPU instruction and loads directly from fixed byte offset +32 (~0.8 ns); (2) Polymorphic IC (2–4 Shapes): branches across up to 4 cached Maps; (3) Megamorphic IC (>4 Shapes) or delete obj.prop: delete detaches the object from the Shape transition tree into slow hash-table Dictionary Mode (~38 ns, ~45x slower!). Similarly, for closures, V8 allocates a single heap Context array per lexical scope shared by all nested functions in that scope—meaning a sibling closure can accidentally keep a 10 MB buffer alive!

Two JavaScript objects share the exact same V8 Hidden Class (Map) if and only if they start from the same prototype/constructor and have the exact same sequence of property names added in the exact same order without executing delete. When every instance returned by a factory function initializes the exact same keys in the exact same order (setting absent optional properties to undefined or null), every instance terminates at the identical leaf Map node, guaranteeing a 1-shape Monomorphic Inline Cache at all downstream read sites.

A Monomorphic IC property load takes ~0.8 ns (1 Map pointer check + 1 fixed-offset memory read), whereas a Megamorphic or Dictionary Mode (delete) lookup takes ~35–45 ns (~45x slower). In production systems (V8 (Node.js, Chrome, Deno, Cloudflare Workers), JavaScriptCore (Structure IDs in Bun and Safari), Fastify's schema compiler, and Pino logger all enforce fixed-shape object construction for monomorphic ICs.), engineers monitor these exact trade-offs to keep p99 latency predictable and prevent resource exhaustion under peak load.

What the interviewer is listening for

  • Traces how V8 Hidden Classes (Maps/Shapes), Inline Caches (ICs), and shared lexical closure Contexts operates in memory step by step rather than giving a vague textbook definition.
  • States the core correctness invariant and explains why edge cases do not violate it.
  • Quantifies time, space, and hardware costs (Monomorphic IC (1 Shape), Polymorphic IC (2–4 Shapes)) with concrete numbers.
  • Identifies real-world failure modes (Conditionally adding optional fields (if (x) obj.x = x)) and explains how production systems mitigate them.

Common mistakes

  • Treating V8 Hidden Classes (Maps/Shapes), Inline Caches (ICs), and shared lexical closure Contexts as a black box without explaining the underlying memory or state transitions.
  • Ignoring worst-case tail latency or memory overhead (Using plain objects {} as dynamic hash maps with arbitrary user-supplied keys (cache[userId] = val; delete cache[oldId])).
  • Forgetting boundary conditions and error recovery (Capturing a huge request object inside a lexical scope that also creates a long-lived callback).

This answer walks

Practice follow-ups

  1. 01How do you use `node --trace-ic` or `--trace-deopt` to find megamorphic property accesses in a hot production path?
  2. 02When should you use a `Map<string, V>` instead of a plain `{ [key: string]: V }` object in V8?

Not started

Sign in to save your learning progress.

Sign in to save