cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Inline caches

Learn how JavaScript engines cache property lookups at each call site, move from monomorphic to megamorphic, and feed JIT optimizers.

By the end, you can
  • 01
    Explain a property access fast pathDescribe how one access site remembers a shape/map and property offset so the next matching object needs only a guard and load.
  • 02
    Read IC statesDistinguish uninitialized, monomorphic, polymorphic, and megamorphic sites, including V8's four-map polymorphic limit.
  • 03
    Connect IC feedback to production codeRecognize keyed, store, call, global, and prototype-chain ICs, then write shape-friendly code without over-optimizing blindly.

Lookups become remembered work

A naive property lookup is a search. The engine checks the object, searches its own properties, and then walks the prototype chain until it finds a matching name or gives up. That flexible rule is why obj.x works even when x lives on a prototype.

Definition

An inline cache is feedback stored at a specific operation site. For a property load, it remembers facts like “objects with shape S store property x at offset k,” so the next matching object can be handled by a shape check and one load.

The “inline” part is historical: early Smalltalk and JavaScript engines patched a tiny fast path near the operation. Modern engines may store feedback in vectors and stubs instead, but the mental model is the same: each call site learns from the shapes it actually sees.

The slow mental model: search own properties, then prototypesPop out in the code editor (opens in a new tab)JavaScript
const record = { user: { name: "Ada" } };const proto = { role: "author" };Object.setPrototypeOf(record.user, proto); function naiveLookup(object, name) {  let current = object;  while (current) {    if (Object.prototype.hasOwnProperty.call(current, name)) {      return current[name];    }    current = Object.getPrototypeOf(current);  }  return undefined;} console.log(naiveLookup(record.user, "name"));console.log(naiveLookup(record.user, "role"));

Line 6 asks whether the current object owns the property. Line 10 moves to the prototype. Engines cannot afford that search on every hot read, so they attach a specialized stub after the first miss proves where the property lives.

Real-life analogyA mail carrier learns the mailbox slot

Imagine a carrier delivering to one building entrance all day. The first visit needs a directory. After that, “street shape plus box number” is enough, until the route includes too many unrelated buildings.

In real life: A new street requires checking the directory
In JavaScript: The first shape at a site uses the slow lookup
In real life: After learning the route, the carrier checks the street sign
In JavaScript: The IC checks the object's shape/map
In real life: The same house number is found in the same slot
In JavaScript: The property offset is loaded directly
In real life: Too many apartment layouts need the central desk
In JavaScript: A megamorphic site falls back to a stub cache or generic lookup

Where the analogy stops: Mail slots do not mutate while the carrier is walking. JavaScript objects and prototypes can change, so engines keep guards, validity checks, and deoptimization paths.

This lesson sits after type feedback and before deeper hidden-class lessons. If the words shape, map, and hidden class are new, use the small definition here and revisit object basics, the prototype chain, classes, and property descriptors. The later Hidden classes & object shapes lesson goes deeper.

Shapes, maps, and offsets

Engines use different names, but the useful idea is shared: objects with the same property layout share a hidden class, map, or shape. That layout stores where each property lives. If x is at offset 0 for shape S, the engine can load slot 0 after checking S.

Same names in the same order create the stable layout you wantPop out in the code editor (opens in a new tab)JavaScript
function makeUser(name) {  return { name, score: 0, active: true };} const ada = makeUser("Ada");const lin = makeUser("Lin");const reordered = { score: 0, name: "Grace", active: true }; console.log(Object.keys(ada).join(","));console.log(Object.keys(lin).join(","));console.log(Object.keys(reordered).join(","));

This snippet only prints key order because browser JavaScript does not expose maps. The real evidence below uses Node 22.23.1 with V8 native syntax in a child process. It is marked non-runnable in the browser editor on purpose.

Node-only proof with %HaveSameMapJavaScript
function Point(x) {  this.x = x;} const a = { x: 1, y: 2 };const b = { x: 3, y: 4 };const c = { y: 2, x: 1 };const p = new Point(1);const q = new Point(2); console.log(`Node ${process.versions.node} (V8 ${process.versions.v8.split(".").slice(0, 2).join(".")})`);console.log("same property order", %HaveSameMap(a, b));console.log("different property order", %HaveSameMap(a, c));console.log("constructor start", %HaveSameMap(p, q));p.y = 2;console.log("after one transition", %HaveSameMap(p, q));q.y = 3;console.log("after both transition", %HaveSameMap(p, q));
Verified runtime fact

Tests run this code in Node 22.23.1 (V8 12.4). Same property order reports true, different order reports false, and adding a property to one constructor instance makes it diverge until the other instance takes the same transition.

The V8 team explains the internal property-store distinction in Fast properties in V8, and the V8 maps docs show that a map is the first pointer used to compare an object's hidden class. Mathias Bynens and Benedikt Meurer's Shapes and Inline Caches article is the high-level companion.

Monomorphic inline caches: one shape, one load

Inline caches belong to operation sites, not to property names globally. Two different obj.x expressions have separate feedback. At one site, the first object misses, the engine runs the slow lookup, then attaches a stub for that shape and offset.

Step through a monomorphic load IC
Step 0 of 7Ready
Your turn: follow the blue line

Watch one property access site learn a shape, then turn the next same-shaped read into a shape check plus one load.

Running in
  1. script
Next: line 13
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function createGetXSite() {  let cachedShape = null;  let cachedOffset = "x";  return function getX(record) {    if (record.shape === cachedShape) {      return record.slots[cachedOffset];    }    cachedShape = record.shape;    return record.slots.x;  };} const a = { shape: "x,y", slots: { x: 1, y: 10 } };const b = { shape: "x,y", slots: { x: 2, y: 20 } };console.log(getX(a));console.log(getX(b));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Step through the replay. The first getX(a) call records the shape and the offset for x. The second call receives a different object with the same shape, so the IC can do exactly what optimizing compilers want: check the map, then load the slot.

What is cached?

The IC does not cache the value 1. It caches a safe recipe: if the receiver has this shape, read this offset. If the shape check fails, the site misses and gathers new feedback.

Polymorphic to megamorphic

Real programs often send two or three stable layouts through one helper. That is polymorphic feedback: the site has a short list of shape-specific stubs. In V8 12.4, the source and tests for this lesson verify the polymorphic map budget as four; a fifth distinct map pushes the site into megamorphic handling. Treat the exact number as implementation detail, not JavaScript semantics.

Step from monomorphic to megamorphic
Step 0 of 7Ready
Your turn: follow the blue line

Step through one access site as it grows from monomorphic to polymorphic, then falls back to a megamorphic stub cache.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const site = { state: "uninitialized", entries: [], stubCache: new Map() }; function readX(record) {  const key = record.shape + ":x";  if (site.state === "megamorphic" && site.stubCache.has(key)) {    return site.stubCache.get(key)(record);  }  if (site.entries.includes(record.shape)) {    return record.slots.x;  }  if (site.entries.length === limit) {    site.state = "megamorphic";    site.stubCache.set(key, (object) => object.slots.x);    return record.slots.x;  }  site.entries.push(record.shape);  site.state = site.entries.length === 1 ? "monomorphic" : "polymorphic";  return record.slots.x;} const records = [  { shape: "S1", slots: { x: 1 } },  { shape: "S2", slots: { x: 2 } },  { shape: "S3", slots: { x: 3 } },  { shape: "S4", slots: { x: 4 } },  { shape: "S5", slots: { x: 5 } },]; for (const record of records) console.log(readX(record));console.log(readX(records[4]));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Playground: feed shapes into one getX site
Teaching-model getX siteJavaScript
const site = createInlineCacheSite("getX");const objects = [  makeObject([["x", 1], ["a", 10]], "x-a"),  makeObject([["a", 20], ["x", 2]], "a-x"),  makeObject([["x", 3], ["b", 30]], "x-b"),  makeObject([["b", 40], ["x", 4]], "b-x"),  makeObject([["c", 50], ["x", 5]], "c-x"),]; for (const object of objects) {  console.log(site.read(object, "x"));}
IC stateuninitialized
0 fast hits0 IC misses0 dictionary lookups0 stub hits
x-aS2

x → a

a-xS4

a → x

x-bS5

x → b

b-xS7

b → x

c-xS9

c → x

No reads yet. Start with the scripted button, then compare manual shapes.

Step 0 of 6uninitialized

Push an object through getX(obj). Same-layout objects should hit quickly; several different layouts push the site toward megamorphic.

This is a browser-side teaching model. It does not inspect your browser engine; it replays the inline-cache state machine described in the lesson.
Inline-cache states and their cost
StateWhat the site has seenTypical cost
UninitializedThe site has no feedback yet.First access must run the normal lookup and attach a stub if the case is cacheable.
MonomorphicOne shape/map has been seen for this property name.One shape check and one offset load on a hit; this is the optimizer's favorite case.
PolymorphicA small set of shapes has been seen. V8 12.4 uses four maps as the limit in its IC policy.A short chain of checks or a compact multi-map guard; still useful feedback but more expensive than one shape.
MegamorphicToo many shapes reached the same site.The site stops growing local stubs and uses broader lookup machinery such as a stub cache keyed by map and name.

V8's megamorphic named-load path uses broader lookup machinery and a stub cache keyed by information such as the receiver map and property name. That keeps one noisy site from growing unlimited local code, but it is slower than a tiny monomorphic guard.

Sort the IC state
  • getX reads objects created by one constructor that initializes x then y.
  • getX handles API rows shaped {x, name} and chart points shaped {label, x}.
  • A hot formatter reads .x from dozens of unrelated JSON objects.
  • A renderer sees four stable view-model shapes and no others.
  • A loop reads items[i] from packed arrays built by the same helper.
  • A plugin hook calls a different callback shape and target for every installed plugin.
Try it yourself
0 of 6 correct

Place each access pattern in the state it is likely to create at one hot site.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Keyed, store, call, and global ICs

Property loads are the easiest IC to picture, but feedback exists for many dynamic operations. A keyed IC for obj[key] has to separate named properties from element access. Arrays add another dimension: packed arrays, holey arrays, integer elements, doubles, and object elements each give different feedback.

Keyed ICs separate names, elements, and holesPop out in the code editor (opens in a new tab)JavaScript
function readKey(target, key) {  return target[key];} const user = { name: "Ada" };const packed = ["zero", "one", "two"];const holey = [];holey[2] = "two"; console.log(readKey(user, "name"));console.log(readKey(packed, 1));console.log(0 in holey);console.log(readKey(holey, 2));

Store ICs learn whether a write updates an existing slot or creates a shape transition. Call ICs learn callable targets and receiver shapes, which is why a monomorphic call site can later be inlined by an optimizing compiler.

A store IC distinguishes update from transitionPop out in the code editor (opens in a new tab)JavaScript
function writeX(object, value) {  object.x = value;  return object.x;} const a = { x: 0, y: 0 };const b = { x: 1, y: 1 };const c = { y: 0 }; console.log(writeX(a, 7));console.log(writeX(b, 8));console.log(Object.keys(c).join(","));console.log(writeX(c, 9));console.log(Object.keys(c).join(","));
A call IC records the target functionPop out in the code editor (opens in a new tab)JavaScript
function render(formatter, value) {  return formatter(value);} function money(amount) {  return "$" + amount.toFixed(2);} function label(value) {  return "item:" + value;} console.log(render(money, 3));console.log(render(money, 4));console.log(render(label, "A"));

Global load ICs do similar work for names such as Math or document. Prototype-chain loads cache not only the receiver shape, but also assumptions that the prototype chain has not changed. V8 describes these dependency checks with validity cells; the later hidden classes and prototypes inside the engine lessons go deeper.

Prototype changes explain why guards are necessaryPop out in the code editor (opens in a new tab)JavaScript
const proto = { x: 1 };const child = Object.create(proto); console.log(child.x);proto.x = 2;console.log(child.x);child.x = 3;console.log(child.x);console.log(proto.x);
Common inline-cache families
IC familySyntaxFeedback it records
Named load ICobj.xCaches a named property lookup and usually records a shape/map plus offset.
Keyed load ICobj[key] or array[index]Separates named keys from element access; arrays also carry packed, holey, integer, double, and object element-kind feedback.
Store ICobj.x = valueCaches writes, including whether the write updates an existing slot or transitions the object to a new shape.
Call ICfn(arg) or obj.method()Records callable targets and receiver shapes; monomorphic call feedback can let an optimizer inline the target.
Global load ICdocument or MathCaches global object or lexical environment lookups with guards that the binding cell is still valid.

CacheIR and other engines

SpiderMonkey documents CacheIR as the core of its speculative optimization strategy: a simple linear bytecode for inline-cache stubs. The official docs describe guards, idempotent operations, and one final result operation. Their example for a fixed property load is essentially guard the input object, guard the shape, then load the fixed slot.

A simplified CacheIR shape for obj.xText
GetProp "x"  GuardToObject input  GuardShape shape=S1  LoadFixedSlotResult slot=0

Jan de Mooij's CacheIR post explains why Firefox moved this logic into a structured representation. The current SpiderMonkey docs say Baseline stubs are generated from CacheIR and Warp can transpile CacheIR snapshots into optimizing compiler IR. JavaScriptCore uses inline caches too; WebKit's Speculation in JavaScriptCore describes how JSC speculates from runtime evidence and guards those assumptions.

Different names, same job

V8 talks about maps, feedback vectors, IC stubs, and optimizing tiers. SpiderMonkey talks about shapes, Baseline IC chains, CacheIR, and Warp. JavaScriptCore talks about structures, inline caches, speculation, and tiers. The shared idea is guarded specialization.

How optimizers use IC feedback

Baseline tiers gather feedback while keeping code quick to generate. When a function gets hot, an optimizing compiler can look at the IC history. A monomorphic property load can become machine code shaped like “compare receiver map with S; if it fails, deopt; otherwise load memory at offset k.” That is the single-memory-load promise in the curriculum summary.

Polymorphic sites may still optimize with a small guard chain. Megamorphic sites are harder: they say “this operation sees many unrelated shapes,” so the compiler has less to specialize and often leaves a call to generic or stub-cache machinery. Call IC feedback works the same way: one stable target can be inlined; many targets make inlining less attractive.

Measure the actual site

You rarely need to chase IC states by hand. Use measuring performance first. If a profile points at a hot property read, then shape consistency is a useful hypothesis to test.

Practical shape-friendly code

The production advice is deliberately boring: initialize all expected properties, do it in the same order, avoid delete on hot objects, and avoid routing many unrelated object types through one tiny hot helper. Do not contort clear code for a theoretical IC win.

Initialize the complete shape up frontPop out in the code editor (opens in a new tab)JavaScript
function User(name, admin = false) {  this.name = name;  this.admin = admin;  this.score = 0;} const ada = new User("Ada");const lin = new User("Lin", true);lin.score = 10; console.log(Object.keys(ada).join(","));console.log(Object.keys(lin).join(","));delete ada.admin;console.log(Object.keys(ada).join(","));

Lines 2 through 4 create the same layout for every User. Line 13 shows the habit to avoid in hot code: deleting a property changes the layout and can push an object away from fast-property storage. Sometimes deletion is the clearest model; just do not assume it is free inside a hot loop.

  • Construct objects with the same fields in the same order.
  • Prefer setting a field to null or undefined over delete when the object remains hot.
  • Split a hot helper if it processes many unrelated shapes and profiling shows that site matters.
  • Keep readability first, then measure before and after.

Common misconceptions

  • “The IC caches values.” It caches a guarded lookup recipe; values can change.
  • “One polymorphic site is always bad.” A small stable set of shapes is normal and often optimized well.
  • “Megamorphic means broken.” It means the engine chose broader machinery for a diverse site; it matters only when profiling says it is hot.
  • “Hidden classes are JavaScript semantics.” They are engine strategies. The language still says property lookup follows objects and prototypes.
  • “Engine internals should dictate every style choice.” Clear data models, tests, and measurements beat folklore.
Similar ideas that are easy to mix up
IdeaWhat it meansDo not confuse it with
UninitializedThe site has no feedback yet.First access must run the normal lookup and attach a stub if the case is cacheable.
MonomorphicOne shape/map has been seen for this property name.One shape check and one offset load on a hit; this is the optimizer's favorite case.
PolymorphicA small set of shapes has been seen. V8 12.4 uses four maps as the limit in its IC policy.A short chain of checks or a compact multi-map guard; still useful feedback but more expensive than one shape.
MegamorphicToo many shapes reached the same site.The site stops growing local stubs and uses broader lookup machinery such as a stub cache keyed by map and name.

Practice exercises

Exercise 1 · Warm-upPredict a monomorphic helper

Read the snippet and type the two printed values.

Starter codePop out in the code editor (opens in a new tab)JavaScript
function getX(object) {
  return object.x;
}
const a = { x: 1, y: 10 };
const b = { x: 2, y: 20 };
console.log(getX(a));
console.log(getX(b));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upRemember V8's small polymorphic budget

    How many maps fit in the V8 polymorphic IC budget discussed in this lesson?

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeRead a holey array clue

      What does the hole check print?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const holey = [];
      holey[2] = "two";
      console.log(0 in holey);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeName the CacheIR guard

        Which CacheIR operation checks the object's shape in the lesson's sequence?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeChoose a production habit

          Name one practical habit that helps a hot site stay shape-friendly.

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            Inline caches quiz · 7 questionsScore: first tries count
            1. Question 1 of 7What does an inline cache remember for a normal obj.x load?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does this shape-order snippet print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const a = { x: 1, y: 2 };
              const b = { y: 2, x: 1 };
              console.log(Object.keys(a).join("|"));
              console.log(Object.keys(b).join("|"));

              Choose an answer to see the explanation.

            3. Question 3 of 7In V8 12.4, what happens after one property access site sees more than four maps?

              Choose an answer to see the explanation.

            4. Question 4 of 7What does the keyed example print for the hole check?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const holey = [];
              holey[2] = "two";
              console.log(0 in holey);

              Choose an answer to see the explanation.

            5. Question 5 of 7Why is monomorphic call feedback valuable to an optimizing compiler?

              Choose an answer to see the explanation.

            6. Question 6 of 7Which CacheIR sequence best matches a simple fixed-slot property load?

              Choose an answer to see the explanation.

            7. Question 7 of 7Which production habit best supports stable shapes?

              Choose an answer to see the explanation.

            Key takeaways

            • Inline caches are per-site feedback: one obj.x expression can be monomorphic while another is megamorphic.
            • Shapes/maps connect property names to offsets, so a cached load can become a shape guard plus one slot load.
            • V8 12.4 uses a four-map polymorphic budget; too many shapes make a site megamorphic and push work to broader caches.
            • Keyed, store, call, global, and prototype ICs all record different runtime facts for later tiers.
            • Write consistent object shapes when it is natural, avoid hot delete, and measure before optimizing.

            Next in this module, Baseline compilers picks up where IC feedback leaves off: fast simple compilers that remove interpreter overhead before the highest optimizing tiers take over.

            CompleteFrontend Clear concepts. Working examples.