Prototypes inside the engine
Learn how prototype maps, validity cells, and shape teleporting let engines cache inherited lookups without breaking JavaScript semantics.
- 01Explain the shortcut to a prototypeFollow how an engine turns
obj.methodinto two quick checks (the object's map and a validity cell) and one read straight from the prototype that has the method. - 02Tell JavaScript rules from engine tricksSay what JavaScript promises about shadowing and prototype changes, and which parts are only tricks used inside V8 and SpiderMonkey.
- 03Predict what a prototype change costsDecide whether a change turns a validity cell invalid, gives the object a new map, throws away fast code, or leaves the shortcut working.
Prototype lookup needs fast guards
Every object can have a prototype: another object it falls back on. When you read a property, JavaScript looks on the object first. If the property is not there, JavaScript looks on the prototype, then on the prototype's prototype, and so on. This line of objects is called the prototype chain.
The chain can change while a program runs. Code can add a property, replace a method, or give an object a new prototype. The JavaScript engine (the program inside the browser or Node that runs your code) must always give the right answer, even after such a change.
Prototype internals are the tricks an engine uses to find inherited properties fast. The engine saves a shortcut to the prototype that has the property. Before it uses the shortcut, it runs a few quick checks, called guards. The guards make sure the chain has not changed since the shortcut was saved.
The rules of the prototype chain are covered in Prototype chain, Object.create, Class inheritance, and Native prototypes. This lesson is about the engine side. The lesson before this one, Functions & closures inside the engine, showed how the engine stores functions. Here you see how it finds methods that live on a prototype.
First, the rules the engine must keep. The code below uses two tools:
Object.create(proto)makes a new, empty object whose prototype isproto.Object.setPrototypeOf(obj, other)gives an existing objectobja new prototype,other.
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 7 makes
child, withprotoas its prototype. - Line 9 reads
child.label.childhas nolabelof its own, so JavaScript finds it onprotoand printsfrom prototype. - Line 10 gives
childits ownlabel. An object's own property hides a prototype property with the same name. This is called shadowing. - Line 11 prints
own label. Line 12 printsproto.label, which is stillfrom prototype. - Lines 13 to 15 put a new
methodonproto. Line 16 calls it throughchildand printsnew method. - Line 17 gives
childa new prototype. Line 18 deletes the ownlabel. Line 19 printsnew prototype, read from the new prototype.
Any shortcut the engine saves must still print these same five lines.
You want a recipe. You check your own recipe card first. If it is not there, you open the family book. The first time, you search page by page. Then you leave a bookmark on the right page, with a sticker that says "still correct".
Next time, you see the sticker and open the book at the bookmark. If someone edits the book, they peel the sticker off. Then you search page by page again, and put down a new bookmark.
- In real life: You check your own recipe card first
- In JavaScript: JavaScript looks on the object first
- In real life: Shared recipes are in the family book
- In JavaScript: Shared methods are on the prototype
- In real life: A bookmark takes you straight to the right page
- In JavaScript: A saved shortcut goes straight to the prototype
- In real life: A sticker on the bookmark says "still correct"
- In JavaScript: A validity cell says the chain has not changed
- In real life: Someone edits the book and peels off the sticker
- In JavaScript: A change to the prototype marks the cell invalid
Prototype maps
The engine keeps a hidden note for every object. The note lists which properties the object has and where each one is stored. V8 (the engine in Chrome and Node) calls this note a map. SpiderMonkey (the engine in Firefox) calls it a shape. Both words mean the same thing here. Your code never sees a map.
Many objects can share one map when they have the same properties, added in the same order. The hidden classes lesson explains maps in depth. Here you need one new idea: an object that other objects use as their prototype gets extra notes, because many objects now depend on it.
The table below lists the engine words used in this lesson. Read each row from left to right: the word, what it means, and why it matters for prototypes.
| Engine word | What it means | Why prototypes need it |
|---|---|---|
| Map (shape in SpiderMonkey) | The engine's hidden note about an object: which properties it has, and where each one is stored. | Many objects can share one map when they get the same properties in the same order. |
| Prototype map | The map of an object that other objects use as their prototype. | V8 adds extra notes to it, such as prototype info and a validity cell, because other objects depend on it. |
| Prototype link in a map | An object's map also records which prototype the object links to. | One map check proves the object still has no own copy of the property and still has the same prototype. Object.setPrototypeOf(obj, other) gives obj a new map. |
| Validity cell | A tiny flag, valid or invalid, that saved shortcuts to a prototype check. | When a prototype on the chain changes, its cell turns invalid, and old shortcuts fail. |
Mathias Bynens and Benedikt Meurer, who worked on V8, describe one useful trick. An object's map also records which prototype the object links to. So one check of the map answers two questions at once:
- Does this object still lack its own copy of the property?
- Does this object still link to the same prototype?
You can see the extra notes in Node with special V8 commands. Commands that start with %, like %DebugPrint, only work when Node runs with the --allow-natives-syntax flag. They are not normal JavaScript, so this code does not run in the browser.
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;- Lines 1 to 6 make a normal object,
proto. - Lines 7 to 9 print the Node and V8 version.
- Line 10 asks V8 whether
protouses its normal, fast way of storing properties. It printstrue. - Line 11 makes
rectwithprotoas its prototype. Line 12 asks again and now printsfalse. - Line 13 prints everything V8 knows about
proto.
The lesson test runs this code in Node 22 with V8 12.4. It only checks facts that stay the same from run to run. The printout contains prototype_map, prototype info, and prototype_validity cell. In this version, %HasFastProperties(proto) is true before the object becomes a prototype and false after. That does not mean lookups on prototypes are slow. It only means V8 now stores and tracks this object in a different way.
You don't need to remember the exact printout. The main point: a prototype is still a normal JavaScript object. But once other objects depend on it, the engine marks it and keeps an eye on it.
Caching inherited property lookups
A method call does two jobs. Take rect.getArea():
- JavaScript finds
getArea. This is a normal property read. - JavaScript calls that function with
thisset torect.
The first job can be slow if the engine walks the whole chain on every call. Step through it below.
Follow one method call. First JavaScript finds the method on the prototype, then it runs it with the numbers from rect.
script
getArea() { return this.width * this.height; },};const rect = { width: 3, height: 4 };Object.setPrototypeOf(rect, rectPrototype); console.log(rect.getArea());- Line 6 makes
rectwith its ownwidthandheight. - Line 7 links
recttorectPrototype. - Line 9 asks for
getArea.rectdoes not have it, so JavaScript finds it on the prototype. - Line 3 runs inside the method. It reads
widthandheightfromrect, becausethisisrect. The method is shared, but the numbers belong to each object.
getArea on rect first. It is not there, so JavaScript follows the link to the prototype and finds it there.To skip the walk next time, the engine saves a shortcut at that line of code. A saved shortcut like this is called a cache. The table below shows what happens on each read. Each row is one moment: what JavaScript does, and what the shortcut saves or does.
| When | What JavaScript does | What the shortcut saves or does |
|---|---|---|
| The first read | Looks on the object, then walks up the prototypes, following the normal rules. | Saves a shortcut, if this kind of read can be saved. |
| A later read, nothing changed | Gives the same answer as a full walk. | Checks the object's map and the validity cell, then reads the prototype's slot straight away. It never saves the old value. |
| A property is added to or deleted from the prototype | Finds the property using the changed prototype. | The prototype gets a new map and the old cell turns invalid. The next read walks again and saves a fresh shortcut. |
| The object gets a new prototype | Looks on the new prototype. | The object gets a new map, so the map check fails, even if the old cell is still valid. |
The inline caches lesson showed shortcuts for an object's own properties. A shortcut for an inherited property saves more:
- The map of the object you read from, like
rect. - Which prototype has the property.
- The slot: the place in that prototype where the value is stored.
- A validity cell: a small flag that says the chain has not changed. The next section explains it.
getArea straight from the prototype.Validity cells
A validity cell is a tiny flag inside the engine. It says either "valid" or "invalid". Many saved shortcuts can share one cell. Here is how V8 uses it:
- When a shortcut is saved, it keeps a link to the current cell.
- If a prototype on the chain changes, the engine marks that cell invalid.
- The next time a shortcut runs, it sees the invalid cell. It walks the chain again and saves a fresh shortcut.
The code below is a small model of this, written in plain JavaScript. It is not the real engine. Step through it to watch one shortcut being saved, used, and refused.
Watch one saved shortcut: the first read saves it, the second uses it, a change marks its cell invalid, and the third read saves it again.
script
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)());- Line 18 is the first read. Nothing is saved yet, so the model walks to the prototype and saves a shortcut.
- Line 19 is the second read. The map matches and the cell is valid, so the model uses the shortcut.
- Line 20 marks the old cell invalid. Line 22 replaces the method with one that returns 20.
- Line 23 is the third read. The old cell is invalid, so the model walks again, saves a fresh shortcut, and prints 20.
The shortcut never saves the value 12. It saves a safe way to reach the slot. So after the change, line 23 prints 20, as JavaScript requires. The price of the change is that old shortcuts stop working and must be saved again.
A valid cell only means one thing: the facts about the chain that this shortcut relied on are still true. It does not mean the method is fast, or that it will never change. Fast machine code for a call can also depend on which exact function it calls. The engine tracks that separately.
V8's article on prototype speed-ups warns that changing Object.prototype is the widest change of all. Almost every object has Object.prototype at the end of its chain. So changing it can mark many cells invalid at once, including the chains of page elements in a browser. That is why adding to global prototypes after an app has started running is expensive.
Shape teleporting in SpiderMonkey
SpiderMonkey is Firefox's engine. It uses the word shape where V8 says map. It has its own trick for long chains, called shape teleporting. The goal: don't check every object in the middle of the chain on every read.
The block below is a sketch, not real code. It uses three made-up steps:
GuardShape(x)checks that objectxstill has the same shape.LoadSlot(x, "y")reads the value ofyfrom its slot inx.holderPrototypeis the prototype that hasy.middlePrototypesits between it andobj.
Simple shortcut for obj.y (checks every object): GuardShape(obj) GuardShape(middlePrototype) GuardShape(holderPrototype) LoadSlot(holderPrototype, "y") SpiderMonkey shape teleporting (checks two): GuardShape(obj) GuardShape(holderPrototype) LoadSlot(holderPrototype, "y") If middlePrototype gets its own "y": change shapes, so the old checks fail- Lines 1 to 5: a simple shortcut checks all three objects, then reads
y. - Lines 7 to 10: shape teleporting checks only
objandholderPrototype, then readsy. - Lines 11 and 12: the rule that keeps this safe. If
middlePrototypegets its owny, SpiderMonkey changes the shapes, so the old checks fail.
obj.y from the same chain. Shape teleporting skips the check on the middle object. SpiderMonkey keeps this safe by changing shapes if the middle object ever gets its own y.Matthew Gaudet's post about SpiderMonkey explains why the rule is needed. Skipping the middle check is only safe if any change in the middle also changes a shape that is checked. SpiderMonkey makes sure of that. It changes shapes when a middle prototype starts shadowing the property, and when a prototype link changes.
A student needs a worksheet. A note on the door says which classroom hands it out, so the student goes straight there. The student does not check every room on the way.
Then a nearer classroom starts handing out the same worksheet. The school must take the old note away at once. Otherwise the student walks past the nearer room to the wrong one.
- In real life: A student wants a worksheet
- In JavaScript: Code reads a property
- In real life: A note names the classroom that has it
- In JavaScript: The shortcut knows which prototype has the property
- In real life: A nearer classroom starts handing it out
- In JavaScript: A middle prototype gets its own copy of the property
- In real life: The school takes the old note away
- In JavaScript: SpiderMonkey changes shapes, so the old shortcut fails
Shape teleporting is not a new JavaScript feature. The rules still say a lookup walks the chain. Shape teleporting only lets SpiderMonkey get the same answer with fewer checks.
Why changing a prototype is slow
You may change a prototype with Object.setPrototypeOf or by assigning to __proto__. Both are allowed. But both break facts that saved shortcuts and fast code rely on. MDN warns that changing an object's prototype is slow in modern engines. The cost is not only in that one line. Other code that was made fast for the old chain can slow down too.
When a function runs many times, V8 turns it into fast machine code. This is called optimizing. If a fact that the fast code relied on stops being true, V8 throws that code away and goes back to slower code. This is called deoptimizing.
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));- Line 8 defines
area, which callsobject.getArea(). - Line 12 tells V8 to get ready to optimize
area. Line 13 runs it 10,000 times. Line 14 tells V8 to optimize it on the next call. - Line 15 prints
before 12, using the fast code. - Line 16 replaces
getAreaon the prototype. Line 19 printsafter 20.
The lesson test runs this with --allow-natives-syntax --trace-opt --trace-deopt --no-concurrent-recompilation. These flags make V8 print when it optimizes and deoptimizes code. The test checks that the output changes from before 12 to after 20. It also checks that V8 prints a line that starts with "marking dependent code" and ends with "for deoptimization, reason: code dependencies". That line appears after the method is replaced.
Replacing a method is not the same as adding a property. The method still sits in the same slot, so a simple shortcut to the slot can still work. But the fast code may have copied the old function into itself, or checked for it. So V8 tracks that separately, and the next call still gives the right answer.
Experiment: change the prototype chain
This playground is a small model of the shortcut. Each change has one of three results:
- The validity cell turns invalid, so the shortcut is refused.
- The object's map changes, so the map check fails.
- The shortcut can still be used.
Press Read area twice first, so there is a shortcut to reuse. Then try each change and read again.
const site = createInheritedLoadSite("getArea");const rect = makeRect(3, 4); site.read(rect); // first read: walk, save a shortcutsite.read(rect); // uses the shortcutrect.width = 5; // shortcut still worksrect.__proto__.perimeter = 18; // new prototype map, cell turns invalidsite.read(rect); // shortcut refused: walk, save againRectMap#2ProtoMap#1cell#1trueemptyemptyemptygetAreaNo reads yet. Press Read area to save the first shortcut.
Press Read area to read rect.getArea. Then change the object or its prototype, and read again.
- Add
perimetertoRect.prototypeafter the shortcut is saved. - Delete a helper from
Object.prototypeafter using it. - Call
Object.setPrototypeOf(rect, otherMethods). - Assign
rect.getArea = function () { return 99; }. - Assign
rect.width = 5, whilegetAreastays on the prototype. - Put a new function in
Rect.prototype.getArea.
Sort each change by what happens to the shortcut in this lesson's model.
Practical use
Most developers never need to think about prototype maps. The useful habits are simple:
- Give each object the right prototype when you create it.
- Don't change prototypes in code that runs very often.
- Don't add to built-in prototypes after the app has started.
- Measure before you rewrite readable code for speed.
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());Line 1 makes one object that holds the shared method. Lines 7 and 12 use Object.create to set the prototype at the moment each object is made. Lines 10 and 15 print 12 and 10. This is different from making an object, using it many times, and then changing its prototype. If a library must add to a prototype, doing it before the busy code starts is less harmful than doing it in the middle.
- Make objects with
Object.create(proto), a class, or a constructor function. Don't change their prototype later. - Don't add to
Object.prototypeor other built-in prototypes in code that runs often. - Changing an object's own data, like
rect.width, is fine. The method's result changes, but the shortcut to the method still works. - Use a profiler (a tool that measures where time goes) before you change your design for prototype speed.
The next lesson, The heap & pointer compression, moves from prototype notes to how values and objects are laid out in memory.
Common misconceptions
- "The engine saves inherited values." It saves a checked way to reach the slot. The value in the slot can still change.
- "Changing a prototype only costs that one line." It can also throw away fast code that relied on the old chain.
- "A prototype map is a JavaScript feature." Maps, shapes, and validity cells belong to the engine. The language does not mention them.
- "Shape teleporting breaks the lookup rules." It skips checks only when shape changes keep the answer correct.
- "All prototype changes cost the same." Adding a prototype property, replacing a method, changing an object's data, and setting a new prototype each break different checks.
The table below compares ideas that look alike. Each row says what the idea means, and what it does not mean.
| Idea | What it means | What it does not mean |
|---|---|---|
| The prototype chain rules | The JavaScript rules say a read checks the object's own properties, then its prototypes. | The rules do not ask for maps, validity cells, saved shortcuts, or shape teleporting. |
| A saved shortcut to a prototype | A checked way to reach the place where the property was found. | A saved copy of the property's value. |
| Replacing a method | JavaScript must call the new function on the very next read. | The same as adding or deleting a property. The slot stays where it is, but fast code that relied on the old function is thrown away. |
| Shape teleporting | SpiderMonkey skips checks on middle prototypes, and changes shapes so this stays correct. | Breaking JavaScript's lookup rules. |
Practice exercises
Type the three values the code prints.
const proto = { price: 100 };
const item = Object.create(proto);
console.log(item.price);
item.price = 120;
console.log(item.price);
console.log(proto.price);It prints 100, then 120, then 100. The own price on item hides the prototype's price, but does not change it.
What does item.tag() print after the prototype method is replaced?
const proto = { tag() { return "old"; } };
const item = Object.create(proto);
proto.tag = function tag() { return "new"; };
console.log(item.tag());It prints new. A shortcut may remember where the slot is, but JavaScript always reads what is in the slot now.
A saved shortcut to an inherited property checks the object's map. What else does V8 check?
The second check is the prototype validity cell. When a prototype changes, its cell turns invalid, and every shortcut that shares it fails quickly.
The tests also run a small Object.setPrototypeOf program: item starts with a prototype whose label is "old", then gets a new prototype whose label is "new". What does item.label print after the switch?
// Problem: this line runs after the app has been busy for a while.
Object.setPrototypeOf(card, premiumCardMethods);const card = Object.create(premiumCardMethods);Create the object with the right prototype from the start. Don't change the prototype of an object that busy code already uses.
Your app changes a prototype every time the page scrolls. Name one safer habit from the Practical use section.
Good answers: create objects with the right prototype, change prototypes before busy code runs, don't add to Object.prototype, and measure first.
Which SpiderMonkey trick lets a shortcut check only the first object and the prototype that has the property?
It is shape teleporting. It is safe only because SpiderMonkey changes shapes when shadowing or a new prototype link would make the shortcut wrong.
Check your understanding
For each question, first work out what JavaScript prints. Then name the engine check that keeps the shortcut correct.
Question 1 of 7In V8, what does a saved shortcut to an inherited property check before it reads?
Choose an answer to see the explanation.
Question 2 of 7What does this shadowing snippet print?
Read the code, then predictconst 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.
Question 3 of 7Why does
Object.setPrototypeOfslow down fast code?Choose an answer to see the explanation.
Question 4 of 7What does this method replacement print?
Read the code, then predictconst 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.
Question 5 of 7What does SpiderMonkey's shape teleporting try to avoid?
Choose an answer to see the explanation.
Question 6 of 7Which change leaves the shortcut to
getAreaworking, but still changes whatgetAreareturns?Choose an answer to see the explanation.
Question 7 of 7What did the Node and V8 printout in this lesson show?
Choose an answer to see the explanation.
Key takeaways
- Every method call starts with a property read. Shortcuts make that read faster. They never change
thisor the shadowing rules. - In V8, an object used as a prototype gets extra notes, such as prototype info and a validity cell.
- A saved shortcut checks the object's map and the validity cell. Then it reads straight from the prototype's slot.
- SpiderMonkey's shape teleporting skips checks on middle prototypes. It stays correct because changes in the middle change a shape that is checked.
- Changing prototypes, or adding to built-in prototypes, after code has run many times can throw away a lot of fast code. Set the prototype when you create the object, and measure first.
In one line: the engine trusts its saved shortcuts to prototypes only while their checks still pass.
Next: The heap & pointer compression shows where these objects, maps, and values live in memory.