cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Prototypes inside the engine

Learn how prototype maps, validity cells, and shape teleporting let engines cache inherited lookups without breaking JavaScript semantics.

By the end, you can
  • 01
    Explain an inherited lookup fast pathTrace how an engine can turn obj.method into a receiver-map check, a validity-cell check, and a direct load from the prototype holder.
  • 02
    Separate semantics from engine strategyDescribe what JavaScript promises for shadowing and prototype changes, then identify which parts are implementation details in V8 and SpiderMonkey.
  • 03
    Predict prototype mutation costsDecide when a change flips validity cells, changes receiver maps, deoptimizes call targets, or leaves an inherited-load cache usable.

Prototype lookup needs fast guards

JavaScript's prototype chain is flexible. A property can live on the receiver, on its direct prototype, or higher up the chain. That flexibility is language semantics: engines must return the same value even after shadowing, method replacement, or Object.setPrototypeOf changes the chain.

Definition

Prototype internals are the engine strategies that make inherited property lookup fast: prototype-specific maps, validity cells, and guarded inline caches that can jump straight to the prototype holding a property when the chain is still safe.

We will not repeat the language semantics covered in Prototype chain, Object.create, Class inheritance, and Native prototypes. Instead, we will look at the engine side. The previous batch lesson, Functions & closures inside the engine, explained function objects and feedback cells. This lesson connects that to inherited methods.

Language behavior the engine must preservePop out in the code editor (opens in a new tab)JavaScript
const proto = {  label: "from prototype",  method() {    return "old method";  },};const child = Object.create(proto); console.log(child.label);child.label = "own label";console.log(child.label);console.log(proto.label);proto.method = function method() {  return "new method";};console.log(child.method());Object.setPrototypeOf(child, { label: "new prototype" });delete child.label;console.log(child.label);

Line 8 reads label from the prototype. Line 9 creates an own property that shadows it, so line 10 prints the own value while line 11 proves the prototype is unchanged. Lines 13 through 16 replace the method on the prototype, and the instance sees the new method. Lines 17 and 18 then change the prototype link and remove the shadow, so the next lookup reaches the new prototype.

Real-life analogyA family recipe book with a current-page mark

Check your own recipe note first, then the family recipe book. If the book's current-page mark is still valid, you can return to that page quickly. When someone changes the page, the old mark can no longer be trusted.

In real life: Check your own recipe note first
In JavaScript: Lookup starts on the receiver object
In real life: Shared recipes live in the family book
In JavaScript: Methods often live on the prototype
In real life: A mark says the page is current
In JavaScript: A validity cell lets a cache trust the prototype chain
In real life: A changed page clears the old mark
In JavaScript: Prototype mutations make old caches miss

Where the analogy stops: A recipe book does not run code. JavaScript has many objects and call sites, and engines must keep exact [[Get]] semantics.

Prototype maps

A map or shape is engine metadata describing an object's layout. The neighbouring hidden-classes lesson goes deeper on map transition trees and descriptors. Here we only need one extra idea: prototypes get special bookkeeping because many receivers can depend on them for inherited lookups.

Prototype maps compared with ordinary maps
Engine ideaPlain meaningWhy it matters for prototypes
Ordinary object mapDescribes an object's own property layout and transitions.Many objects can share it when their fields are added in the same order.
Prototype mapA map used by an object that other objects point to as a prototype.V8 gives prototypes special bookkeeping such as prototype info and a validity cell.
Prototype link in a mapThe receiver's shape/map can guard both property absence and the [[Prototype]] link.Changing Object.setPrototypeOf(receiver, ...) gives the receiver a different map.
Validity cellA small cell checked by cached inherited loads.If a relevant prototype changes, old cells become invalid and existing caches miss.

Mathias Bynens and Benedikt Meurer describe a key trick used by engines: the receiver's shape can include the prototype link. A single receiver-map check can therefore prove both “this object still lacks the property” and “this object still points to that prototype.”

Node-only V8 proof: a used prototype gets prototype bookkeepingJavaScript
const proto = {  kind: "rect",  getArea() {    return 12;  },};console.log(  `Node ${process.versions.node} (V8 ${process.versions.v8.split(".").slice(0, 2).join(".")})`,);console.log("before fast properties", %HasFastProperties(proto));const rect = Object.create(proto);console.log("after used as prototype", %HasFastProperties(proto));%DebugPrint(proto);void rect;
Verified runtime fact

The tests run this in Node 22 with V8 12.4 and assert stable facts only: the debug print contains prototype_map, prototype info, and prototype_validity cell. In this build, %HasFastProperties(proto) is true before the object becomes a prototype and false after; that does not mean prototype lookups are slow, only that prototype bookkeeping is not the same as ordinary fast-property storage.

The important lesson is not the exact DebugPrint text. It is that a prototype is still a JavaScript object, but the engine marks and tracks it differently once other objects depend on it.

Caching inherited property lookups

Every method call begins with a property load. rect.getArea() first loads getArea, then calls the function with rect as this. That first step can be expensive if the engine walks the chain every time.

Step through an inherited method lookup
Step 0 of 6Ready
Your turn: follow the blue line

Walk one inherited method call. The replay separates the receiver's own fields, the prototype link, the inherited method, and the final result.

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
  getArea() {    return this.width * this.height;  },};const rect = { width: 3, height: 4 };Object.setPrototypeOf(rect, rectPrototype); console.log(rect.getArea());
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. Line 6 creates the receiver with width and height. Line 7 links the prototype. Line 9 asks for getArea. The method body on line 3 still reads data from the receiver, not from the prototype, because the call uses this = rect.

How an inherited load becomes a cacheable recipe
MomentWhat JavaScript doesWhat a cache can remember
First inherited readWalk the receiver and prototypes using JavaScript semantics.Attach a guarded recipe if the case is cacheable.
Cache hitCheck the receiver map and the cached validity cell.Load the holder's slot directly; do not cache the old value.
Prototype shape changeThe prototype's map changes and the old validity cell is no longer trusted.The next read misses, walks again, and can attach a fresh cache.
Receiver prototype link changeThe receiver gets a new map because the shape carries the prototype link.The receiver-map guard fails even if the old cell is still valid.

The inline-caches lesson introduced receiver-map guards for own properties and briefly mentioned prototype-chain ICs. Here the cached recipe is richer: receiver map, holder prototype, offset, and a validity cell that says the relevant chain has not changed.

Validity cells

A validity cell is a tiny piece of engine state that many optimized paths can check. In V8's prototype-load model, the cached load records the current cell. If a relevant prototype changes, the old cell becomes invalid. The next cache hit fails the cell check and falls back to a fresh lookup.

Step through a cached prototype lookup
Step 0 of 12Ready
Your turn: follow the blue line

Watch a cached inherited load: first miss, second hit, prototype mutation invalidates the old cell, then the next lookup refreshes the 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 cache = { receiverMap: null, holder: null, offset: null, cell: null };const rectProto = { slots: { getArea() { return 12; } } };const rect = { map: "RectMap#1", prototype: rectProto }; function loadGetArea(receiver) {  if (cache.receiverMap === receiver.map && cache.cell?.valid) {    return cache.holder.slots[cache.offset];  }  const holder = receiver.prototype;  cache.receiverMap = receiver.map;  cache.holder = holder;  cache.offset = "getArea";  cache.cell = validityCell;  return holder.slots.getArea;} console.log(loadGetArea(rect)());console.log(loadGetArea(rect)());validityCell.valid = false;validityCell = { valid: true };rectProto.slots.getArea = () => 20;console.log(loadGetArea(rect)());
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.

The important detail is that the cache does not store the value 12. It stores a safe path to the slot. After the mutation, line 23 returns 20 because JavaScript semantics win. The cost is paid by invalidating old assumptions and refreshing the cache.

Validity is about assumptions, not morality

A valid cell only means “the prototype-chain facts this cache depended on are still true.” It does not say the method is pure, fast, or unchanged forever. Optimized calls may also keep separate dependencies on a specific function target.

V8's public prototype-optimization article says changing Object.prototype is especially broad: it can invalidate prototype loads below it, including DOM-related chains in browsers. That is why patching global prototypes after an app has warmed up is expensive.

Shape teleporting in SpiderMonkey

SpiderMonkey, Firefox's engine, uses the word shape where V8 often says map. Its “shape teleporting” optimization answers the same pressure: long prototype chains should not require a guard for every intermediate object on every hot lookup.

Shape teleporting as a teaching modelText
Naive prototype-chain cache for obj.y:  GuardShape(obj)  GuardShape(obj.[[Prototype]])  GuardShape(intermediatePrototype)  GuardShape(holderPrototype)  LoadSlot(holderPrototype, "y") SpiderMonkey shape teleporting model:  GuardShape(obj)  GuardShape(holderPrototype)  LoadSlot(holderPrototype, "y")  Reshape holders if an intermediate prototype starts shadowing "y"

Matthew Gaudet's SpiderMonkey post explains the rule carefully: if a property is found on holder prototype B, the cache would like to guard only the receiver and B. That is safe only if SpiderMonkey reshapes objects when an intermediate prototype starts shadowing the property, or when prototype links mutate.

Real-life analogyA shortcut note to the right classroom

A note can send a student straight to the classroom with a worksheet instead of checking every room. If a nearer classroom starts handing out the same worksheet, the note must change immediately or the student goes to the wrong place.

In real life: A student asks for a worksheet
In JavaScript: A lookup asks for a property
In real life: A note points to the right classroom
In JavaScript: The cache knows the holder prototype
In real life: A nearer classroom starts handing it out
In JavaScript: Shadowing forces reshaping so old shortcuts fail
In real life: Classrooms move, so the note is removed
In JavaScript: Prototype mutation invalidates shortcut assumptions

Where the analogy stops: A paper note cannot update itself. Shape teleporting is mechanical: SpiderMonkey reshapes at the right mutations so shortcuts stay correct.

This is an engine strategy, not a new JavaScript feature. The specification still says lookup walks the chain. Shape teleporting is how SpiderMonkey can make the common case behave as if it walked, while doing fewer checks.

Why changing a prototype is slow

Object.setPrototypeOf and __proto__ assignment are legal, but they change the assumptions optimized code uses. MDN warns that changing an object's [[Prototype]] is slow in modern engines, and the effects can extend beyond the single statement because other code may have optimized around the old chain.

Node-only trace: optimized code is marked for deoptimization after a prototype method changesJavaScript
const proto = {  getArea() {    return 12;  },};const rect = Object.create(proto); function area(object) {  return object.getArea();} %PrepareFunctionForOptimization(area);for (let i = 0; i < 10000; i += 1) area(rect);%OptimizeFunctionOnNextCall(area);console.log("before", area(rect));proto.getArea = function getArea() {  return 20;};console.log("after", area(rect));
Verified runtime fact

The lesson test runs this with --allow-natives-syntax --trace-opt --trace-deopt --no-concurrent-recompilation. It proves the JavaScript output changes from before 12 to after 20, and V8 prints a stable “marking dependent code … for deoptimization, reason: code dependencies” line after the prototype method is replaced.

Replacing a method value is not identical to adding a property. A simple inherited load can still know the slot, but optimized call code may have inlined or guarded the old function target. Engines keep separate dependencies so the next call remains correct.

Experiment: mutate the prototype chain

Use this teaching model to separate three outcomes: a validity-cell invalidation, a receiver-map miss, and a lookup that can stay cached. Start with two reads so the cache has something to reuse, then try each mutation.

Playground: mutate the prototype chain
Teaching-model inherited lookup siteJavaScript
const site = createInheritedLoadSite("getArea");const rect = makeRect(3, 4); site.read(rect);                 // cold miss, installs cachesite.read(rect);                 // cache hitrect.width = 5;                  // cached lookup can stayrect.__proto__.perimeter = 18;   // prototype map + validity cell changesite.read(rect);                 // validity-cell miss, then refreshed cache
Cache state0 hits · 0 misses
0 cache hits0 misses0 invalidations0 call-target changes
receiver mapRectMap#2
prototype mapProtoMap#1
validity cellcell#1
cell is validtrue
cached receiver mapempty
cached holder mapempty
cached cellempty
prototype keysgetArea

No reads yet. Press Read area to attach the first cache entry.

Step 0 of 6ready

Start by reading getArea, then mutate the receiver or prototype and read again.

This is a teaching model; it does not inspect your browser's engine. It models receiver-map guards, validity cells, and method-call dependencies described in the lesson.
What breaks the inherited lookup cache?
  • Add perimeter to Rect.prototype after caches are warm.
  • Delete a helper from Object.prototype after using it.
  • Call Object.setPrototypeOf(rect, otherMethods) on a receiver.
  • Assign rect.getArea = function () { return 99; }.
  • Assign rect.width = 5 while getArea still lives on the prototype.
  • Replace Rect.prototype.getArea with a new function at the same property.
Try it yourself
0 of 6 correct

Sort each mutation by the guard it breaks in the lesson's teaching model.

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

Practical use

Most developers should not micro-manage prototype maps. The practical advice is simple: create objects with the prototype you want, avoid changing prototypes on hot paths, avoid patching native prototypes after startup, and measure before rewriting readable code.

Create with the right prototype up frontPop out in the code editor (opens in a new tab)JavaScript
const rectMethods = {  getArea() {    return this.width * this.height;  },}; const card = Object.create(rectMethods);card.width = 3;card.height = 4;console.log(card.getArea()); const nextCard = Object.create(rectMethods);nextCard.width = 5;nextCard.height = 2;console.log(nextCard.getArea());

Lines 7 and 12 use Object.create to choose the prototype at construction time. That is different from taking a warmed object and changing its prototype link later. If a framework or polyfill must patch a prototype, doing it before the app's hot code runs is less disruptive than changing it mid-flight.

  • Create objects with Object.create(proto), a class, or a constructor instead of changing prototypes later.
  • Do not patch Object.prototype or native prototypes in application hot paths.
  • Changing ordinary receiver data is fine; the method result can change without invalidating the inherited method lookup.
  • Use a profiler before changing architecture for prototype performance.

The next lesson, The heap & pointer compression, moves from prototype metadata to how values and objects are placed in memory.

Common misconceptions

  • “The engine caches inherited values.” It caches guarded lookup recipes; the value in the slot can still change.
  • “Changing one prototype only costs that one line.” It can invalidate optimized code that depended on the old chain.
  • “A prototype map is a JavaScript feature.” Maps, shapes, and validity cells are implementation details.
  • “Shape teleporting skips semantics.” It skips repeated checks only when reshaping rules keep semantics correct.
  • “All prototype mutations are equally bad.” Adding a prototype property, replacing a method value, changing receiver data, and setting a new prototype break different assumptions.
Ideas that are easy to confuse
IdeaMeansDo not confuse it with
Prototype-chain semanticsThe specification says how [[Get]] searches own properties and prototypes.It does not require maps, validity cells, inline caches, or shape teleporting.
Prototype lookup cacheA guarded recipe for where a property was found on a prototype.A permanent copy of the property value.
Changing a method valueJavaScript must call the new function on the next lookup.The same thing as adding or deleting a property; the slot may stay in place while call-target dependencies deopt.
Shape teleportingSpiderMonkey can skip intermediate prototype shape checks when reshaping rules keep it correct.Skipping JavaScript's observable prototype-chain rules.

Practice exercises

Exercise 1 · Warm-upPredict prototype shadowing

Type the three values printed by the snippet.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const proto = { price: 100 };
const item = Object.create(proto);
console.log(item.price);
item.price = 120;
console.log(item.price);
console.log(proto.price);

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

    Exercise 2 · Warm-upReplace a prototype method

    What does item.tag() print after the prototype method is replaced?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const proto = { tag() { return "old"; } };
    const item = Object.create(proto);
    proto.tag = function tag() { return "new"; };
    console.log(item.tag());

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

      Exercise 3 · PracticeName the second guard

      Which guard does V8 check besides the receiver map for a cached inherited load?

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

        Exercise 4 · PracticeFind the slow bug

        In the runnable Object.setPrototypeOf exercise from the tests, what label prints after switching to the new prototype?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        // Bug: this runs after the app has warmed up.
        Object.setPrototypeOf(card, premiumCardMethods);

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

          Exercise 5 · ChallengeApply it to a real app

          Your app patches a prototype during a scroll interaction. Name one safer habit from the practical section.

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

            Exercise 6 · ChallengeName the SpiderMonkey shortcut

            Which SpiderMonkey optimization lets a cache jump from receiver shape to holder shape in the lesson?

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

              Check your understanding

              Answer by separating JavaScript semantics from the engine guard that keeps a fast path safe.

              Prototype internals quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does a cached inherited property load usually guard in the V8 model from the lesson?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does this shadowing snippet print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const proto = { price: 100 };
                const item = Object.create(proto);
                console.log(item.price);
                item.price = 120;
                console.log(item.price);
                console.log(proto.price);

                Choose an answer to see the explanation.

              3. Question 3 of 7Why does changing a prototype link with Object.setPrototypeOf hurt optimized code?

                Choose an answer to see the explanation.

              4. Question 4 of 7What does this method replacement print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const proto = { tag() { return "old"; } };
                const item = Object.create(proto);
                proto.tag = function tag() { return "new"; };
                console.log(item.tag());

                Choose an answer to see the explanation.

              5. Question 5 of 7What is SpiderMonkey's shape teleporting trying to avoid?

                Choose an answer to see the explanation.

              6. Question 6 of 7Which change can leave the inherited lookup cache usable, while still changing the method's result?

                Choose an answer to see the explanation.

              7. Question 7 of 7What did the Node/V8 debug probe prove for this lesson?

                Choose an answer to see the explanation.

              Key takeaways

              • Inherited method calls begin with a property lookup; caches speed up that lookup without changing this or shadowing rules.
              • V8 prototype maps carry prototype-specific bookkeeping such as prototype info and prototype validity cells.
              • A cached inherited load can check the receiver map and validity cell, then load directly from the holder prototype's slot.
              • SpiderMonkey shape teleporting skips intermediate prototype guards only because reshaping rules invalidate unsafe shortcuts.
              • Changing prototypes or patching global prototypes after warm-up can invalidate broad engine assumptions; create with the right prototype and measure first.

              One-line summary: prototype fast paths work by trusting guarded chain facts until a prototype or receiver-map change proves those facts stale.

              Next: The heap & pointer compression explains where these objects, maps, and values live in memory.

              CompleteFrontend Clear concepts. Working examples.