JavaScript Inline Caching: A Complete Tutorial
Master inline caching in JavaScript engines. Covers monomorphic, polymorphic, and megamorphic inline caches, how V8 caches property access and function calls, cache state transitions, and coding patterns that keep inline caches fast.
Inline caching (IC) is V8's primary optimization for property access and function calls. Instead of looking up property locations from scratch every time, V8 remembers where it found a property last time and checks that location first. This transforms hash-table lookups into direct memory reads.
For hidden classes that inline caches depend on, see V8 Hidden Classes in JavaScript: Full Tutorial.
How Inline Caches Work
Every property access site in your code has an associated inline cache. The first time V8 accesses a property, it records the object's hidden class and the property's memory offset. Subsequent accesses check if the hidden class matches and, if so, read the offset directly.
// Consider this property access:
function getX(point) {
return point.x; // <-- This is a property access site with an IC
}
// FIRST CALL: IC is uninitialized
getX({ x: 1, y: 2 });
// V8 does a full property lookup:
// 1. Get hidden class of point -> Map{x@0, y@8}
// 2. Search Map for property "x" -> found at offset 0
// 3. Read value at offset 0 -> 1
// 4. CACHE: Store (Map{x@0, y@8}, offset=0) in the IC
// SECOND CALL: IC hit (monomorphic)
getX({ x: 3, y: 4 });
// V8 uses the cached information:
// 1. Get hidden class of point -> Map{x@0, y@8}
// 2. CHECK: Does it match cached Map? YES
// 3. Read value at cached offset 0 -> 3
// SKIP the property name lookup entirely
// This is ~10x faster than a full lookup
// Each IC has a state:
// UNINITIALIZED -> MONOMORPHIC -> POLYMORPHIC -> MEGAMORPHIC
// |
// v
// GENERIC
// (optimal) (slowest)Monomorphic Inline Caches
// MONOMORPHIC: The IC has seen exactly ONE hidden class
// This is the fastest state
// All objects created the same way share a hidden class
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
function distance(p) {
// Two IC sites: p.x and p.y
return Math.sqrt(p.x * p.x + p.y * p.y);
}
// Every call uses objects with the same hidden class
const points = [];
for (let i = 0; i < 10000; i++) {
points.push(new Point(i, i * 2));
}
// All of these hits are monomorphic IC hits
points.forEach(distance);
// p.x IC: always Map{x@0, y@8} -> offset 0
// p.y IC: always Map{x@0, y@8} -> offset 8
// MONOMORPHIC FUNCTION CALLS
function process(handler) {
handler(); // IC caches the function reference
}
function doWork() { /* work */ }
// If always called with the same function, the call IC is monomorphic
for (let i = 0; i < 10000; i++) {
process(doWork); // Monomorphic: always the same function
}
// V8 may even inline doWork into process when it is monomorphic
// Eliminating the function call overhead entirely
// MONOMORPHIC PROPERTY STORES
function setName(obj, name) {
obj.name = name; // Store IC: caches where to write
}
// Consistent shapes keep the store IC monomorphic
const users = [
{ name: "", age: 0 },
{ name: "", age: 0 },
{ name: "", age: 0 },
];
users.forEach((u) => setName(u, "Alice")); // Monomorphic store ICPolymorphic Inline Caches
// POLYMORPHIC: The IC has seen 2-4 different hidden classes
// V8 creates a small lookup table of (Map, offset) pairs
function getName(obj) {
return obj.name; // This IC site
}
// Two different shapes
getName({ name: "Alice", age: 30 }); // Shape 1: {name@0, age@8}
getName({ name: "Bob", role: "admin" }); // Shape 2: {name@0, role@8}
// The IC is now polymorphic with two entries:
// IC table:
// Map1{name@0, age@8} -> offset 0
// Map2{name@0, role@8} -> offset 0
//
// On each access, V8 checks both maps in order
// Still fast (linear scan of 2-4 entries) but not as fast as monomorphic
// POLYMORPHIC WITH DIFFERENT OFFSETS
function getValue(obj) {
return obj.value;
}
getValue({ value: 1 }); // {value@0}
getValue({ type: "a", value: 2 }); // {type@0, value@8}
getValue({ id: 0, type: "b", value: 3 }); // {id@0, type@8, value@16}
// IC table for getValue:
// Map1{value@0} -> offset 0
// Map2{type@0, value@8} -> offset 8
// Map3{id@0, type@8, value@16} -> offset 16
//
// Even though the property name is the same, different shapes
// store it at different offsets
// V8 handles up to 4 entries polymorphically
// After 4, it transitions to MEGAMORPHIC
// TurboFan handles polymorphic ICs with multi-way checks:
// Pseudocode for optimized getValue:
// map = LoadMap(obj)
// if (map === Map1) return Load(obj, 0)
// if (map === Map2) return Load(obj, 8)
// if (map === Map3) return Load(obj, 16)
// // fallback to generic lookupMegamorphic Inline Caches
// MEGAMORPHIC: The IC has seen 5+ different hidden classes
// V8 gives up per-site caching and uses a global lookup cache
function process(item) {
return item.name; // This IC site
}
// Each call with a differently-shaped object degrades the IC
process({ name: "a" });
process({ name: "b", x: 1 });
process({ name: "c", x: 1, y: 2 });
process({ name: "d", x: 1, y: 2, z: 3 });
process({ name: "e", x: 1, y: 2, z: 3, w: 4 });
// IC is now MEGAMORPHIC after 5 different shapes
// Megamorphic IC behavior:
// - Uses a global "stub cache" (MegamorphicLoadIC)
// - Does a hash table lookup on every access
// - ~10x slower than monomorphic
// - Cannot be inlined by TurboFan
// - Stays megamorphic forever (does not recover)
// DIAGNOSING MEGAMORPHIC ICS
// Node.js: node --trace-ic script.js
// Output: LoadIC at position 42: [megamorphic] <name>
// COMMON CAUSES OF MEGAMORPHIC ICS
// 1. Processing heterogeneous data
function displayItem(item) {
// Each item type has a different shape
return `${item.name}: ${item.value}`;
}
displayItem({ name: "Product", value: 29.99, sku: "A1" });
displayItem({ name: "Service", value: 99, duration: "1h" });
displayItem({ name: "Bundle", value: 149, items: [] });
displayItem({ name: "Digital", value: 9.99, format: "pdf" });
displayItem({ name: "Physical", value: 39, weight: 2.5 });
// 5 shapes -> megamorphic
// FIX: Normalize to a common shape
function normalizeItem(item) {
return {
name: item.name,
value: item.value,
type: item.type || "unknown",
metadata: {}, // Gather extra fields here
};
}
const normalized = items.map(normalizeItem);
normalized.forEach(displayItem); // All same shape -> monomorphic
// 2. Mixing class instances with plain objects
class User { constructor(n) { this.name = n; this.type = "user"; } }
class Admin { constructor(n) { this.name = n; this.type = "admin"; } }
function greet(person) { return person.name; }
greet(new User("A"));
greet(new Admin("B"));
greet({ name: "C", type: "guest" });
greet({ name: "D" });
greet({ name: "E", id: 1 });
// Mix of constructors and object literals -> megamorphicIC Performance Comparison
// Benchmarking IC states
function benchmarkIC() {
const iterations = 10_000_000;
// MONOMORPHIC: All same shape
const monoObjects = [];
for (let i = 0; i < 100; i++) {
monoObjects.push({ value: i, type: "a" });
}
function readMono(obj) { return obj.value; }
console.time("Monomorphic");
for (let i = 0; i < iterations; i++) {
readMono(monoObjects[i % 100]);
}
console.timeEnd("Monomorphic");
// ~15ms
// POLYMORPHIC: 3 shapes
const polyObjects = [];
for (let i = 0; i < 100; i++) {
const mod = i % 3;
if (mod === 0) polyObjects.push({ value: i, a: 1 });
else if (mod === 1) polyObjects.push({ value: i, b: 2 });
else polyObjects.push({ value: i, c: 3 });
}
function readPoly(obj) { return obj.value; }
console.time("Polymorphic (3)");
for (let i = 0; i < iterations; i++) {
readPoly(polyObjects[i % 100]);
}
console.timeEnd("Polymorphic (3)");
// ~35ms
// MEGAMORPHIC: 10+ shapes
const megaObjects = [];
for (let i = 0; i < 100; i++) {
const obj = { value: i };
obj["prop" + (i % 10)] = true; // 10 different shapes
megaObjects.push(obj);
}
function readMega(obj) { return obj.value; }
console.time("Megamorphic");
for (let i = 0; i < iterations; i++) {
readMega(megaObjects[i % 100]);
}
console.timeEnd("Megamorphic");
// ~150ms
}
benchmarkIC();Caching Function Calls
// V8 also uses ICs for function call targets
// MONOMORPHIC CALL IC
class Handler {
process(data) { return data * 2; }
}
const handler = new Handler();
function runHandler(h) {
return h.process(42); // Call IC: caches Handler.prototype.process
}
for (let i = 0; i < 10000; i++) {
runHandler(handler); // Monomorphic: always same method
}
// TurboFan can inline Handler.prototype.process into runHandler
// POLYMORPHIC CALL IC
class Logger { handle(d) { console.log(d); } }
class Counter { handle(d) { return d + 1; } }
function dispatch(obj, data) {
return obj.handle(data); // Call IC
}
const logger = new Logger();
const counter = new Counter();
dispatch(logger, "msg"); // IC learns Logger.prototype.handle
dispatch(counter, 42); // IC adds Counter.prototype.handle
// Polymorphic with 2 entries - still reasonable
// BUILTIN FUNCTION ICS
// V8 also caches calls to built-in methods
const arr = [3, 1, 4, 1, 5];
function getLength(a) {
return a.length; // IC for .length on Array
}
getLength(arr); // Caches: Array shape -> length offset
getLength("hello"); // Different shape! Now polymorphic: Array + String
// Avoid mixing array and string .length in the same call site
// CALL SITE SPLITTING
// V8 creates separate ICs for each call site
function processAll(items) {
for (const item of items) {
item.validate(); // IC site 1
item.transform(); // IC site 2
item.save(); // IC site 3
}
}
// Each method call has its own IC, independent of the others
// If all items have the same shape, all three ICs are monomorphic| IC State | Max Shapes | Lookup Speed | TurboFan Optimization |
|---|---|---|---|
| Uninitialized | 0 | N/A | None |
| Monomorphic | 1 | Direct offset (fastest) | Inlined, type-specialized |
| Polymorphic | 2-4 | Linear check (fast) | Multi-way branch |
| Megamorphic | 5+ | Hash lookup (slow) | Generic (no specialization) |
Rune AI
Key Insights
- Inline caches store the hidden class and property offset from the last property access at each code location: Subsequent accesses with the same hidden class skip the full lookup and read the offset directly
- Monomorphic ICs (one shape) are 10x faster than megamorphic ICs (5+ shapes): Monomorphic access compiles to a single map check plus a direct memory load instruction
- Polymorphic ICs handle 2-4 shapes with a linear check and remain reasonably fast: TurboFan generates multi-way type dispatch code for polymorphic property accesses
- Megamorphic ICs are permanent for a compilation and fall back to hash-table lookups: Once an IC goes megamorphic, it cannot return to a faster state without recompilation
- Normalizing heterogeneous objects to a common shape prevents megamorphic IC degradation: A normalization function that maps diverse objects to one consistent shape keeps all downstream ICs monomorphic
Frequently Asked Questions
Can a megamorphic IC become monomorphic again?
Do inline caches work for bracket notation (obj[key])?
How many polymorphic entries trigger megamorphic?
Do inline caches affect Map and Set operations?
Conclusion
Inline caching is V8's most important optimization for property access performance. Monomorphic ICs provide direct offset reads at near-C-struct speed. Polymorphic ICs handle a few shapes efficiently. Megamorphic ICs fall back to slow generic lookups. Writing code with consistent object shapes keeps ICs monomorphic. For how V8 creates the hidden classes that ICs cache, see V8 Hidden Classes in JavaScript: Full Tutorial. For optimizing object creation to maintain IC stability, review Optimizing JS Object Creation for V8 Engine.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.