Writing engine-friendly code
Learn practical JavaScript habits for stable shapes, packed arrays, predictable call sites, and measured hot-path optimization.
- 01Build objects the same wayGive objects of one kind the same properties, in the same order, and explain why that helps hot code.
- 02Spot variety at one lineTell apart the two notes the engine keeps: which function a call runs, and which object shape a property read sees.
- 03Measure before changing codeRecord a profile of a real slow action before you use any of these habits in real code.
Measure before changing code
This lesson is about small habits that help the JavaScript engine run your code fast. The engine is the part of the browser (or of Node) that runs your JavaScript.
These habits only matter in hot code. Hot code is code that runs thousands of times, in a spot you have measured as slow. Everywhere else, write the clearest code you can.
Engine-friendly code is clear JavaScript that does the same repeated work in the same way each time. When the work looks the same each time, the engine can prepare a faster path for it.
Engines already make ordinary code fast. You don't need to guess what the engine wants in every file. Before you add any habit from this lesson, check three things:
- A profile points at the slow spot. A profile is a recording, made with the browser's performance tools, of where the time goes.
- A user can feel the slow part, such as a checkout button that takes a long time.
- A quick test before and after your change shows that it helped.
This lesson puts ideas from earlier lessons to work: hidden classes, inline caches, elements kinds, type feedback, and speculation and deoptimization.
Build similar objects the same way
Inside the engine, each object has a shape. A shape is a short note the engine keeps about an object. It lists which property names the object has, and in what order they were added. Objects built the same way share one shape.
So here is a simple habit for hot code: give every object of one kind the same properties, in the same order.
A class with a constructor makes this easy. The constructor is the function that runs each time you write new Order(...). It sets up every new order in the same steps.
class Order { constructor(item, qty) { this.item = item; this.qty = qty; }} const first = new Order("tea", 3);const second = new Order("coffee", 2);console.log(Object.keys(first).join(","));console.log(Object.keys(second).join(","));Line by line:
- Line 1 starts the
Orderclass. - Lines 2 to 4 are the constructor. It writes
itemfirst, thenqty(short for quantity). - Lines 8 and 9 make two orders: one for tea and one for coffee.
- Lines 10 and 11 print the property names of each order.
Object.keysgives the names in the order they were added, and.join(",")puts commas between them. Both lines printitem,qty.
item first and qty second.The values are different, but the names arrive in the same order. So both orders get the same shape.
Think of a kitchen where the spoons always go in the first drawer. You reach for a spoon without looking. Now picture a home where each person puts the spoons in a different drawer. Each time, you have to open drawers and search first.
- In real life: Spoons always go in the first drawer
- In JavaScript: Every order gets
itemfirst, thenqty - In real life: You grab a spoon without looking
- In JavaScript: The engine already knows where
qtyis - In real life: Spoons in different drawers mean a search
- In JavaScript: Objects built in different orders need extra checks
A replay of real calls to the Order class. It shows how the orders are built. It does not look inside the engine.
script
constructor(item, qty) { this.item = item; this.qty = qty; }} const first = new Order("tea", 3);const second = new Order("coffee", 2);console.log(Object.keys(first).join(","));console.log(Object.keys(second).join(","));Give every field a value at the start
Some fields are optional. For example, an order may or may not have a note. Optional does not have to mean "added later". In the constructor, you can give the field the value undefined right away. Then every order has a note, even when it is empty.
Here is what happens when two objects get their fields later, in different orders:
const a = {};a.item = "tea";a.qty = 3; const b = {};b.qty = 3;b.item = "tea"; console.log(Object.keys(a).join(","));console.log(Object.keys(b).join(","));- Lines 1 to 3 make
a, then additem, thenqty. - Lines 5 to 7 make
b, then add the same two names the other way round. - Line 9 prints
item,qty. Line 10 printsqty,item.
The two objects have the same names, but not in the same order. So they do not share a shape. One constructor stops this from happening by accident, because it always writes the fields in one order.
Don't write a constructor for an object you make only once. Use one when many objects of one kind go through the same repeated work. The goal is one simple rule for building them. You don't need classes everywhere.
Avoid delete and array holes
The delete keyword removes a property from an object. In hot code this can slow things down. The object changes after the engine has already learned its shape.
If a field is optional, set it to undefined instead. The property stays. Only its value is empty.
const order = { item: "tea", qty: 3, note: "hot" };delete order.note;order.note = undefined;console.log(Object.keys(order).join(",")); const scores = [1, , 3];const filled = Array.from({ length: 3 }, () => 0);console.log(1 in scores);console.log(1 in filled);console.log(new Array(3).length, filled.length);First, the object part:
- Line 1 makes an order with three properties.
- Line 2 deletes
note. Now the order has nonoteat all. - Line 3 adds
noteback, with the valueundefined. - Line 4 prints
item,qty,note.
note out of the order. Line 3 puts the name back with the value undefined, so the keys print item,qty,note.Arrays can have a similar gap, called a hole. A hole is an index with no value at all, not even undefined. Now the array part:
- On line 6, the two commas side by side in
[1, , 3]leave index 1 empty. That is a hole. - Line 7 uses
Array.fromto make an array of length 3. It fills every index with0. - The
inoperator checks if an index (or a property name) exists. Line 8 printsfalse, because index 1 ofscoresis a hole. - Line 9 prints
true, because index 1 offilledholds a real0. - Line 10 prints
3 3.new Array(3)makes an array of length 3 that holds only holes.filledalso has length 3.
scores, the spot at index 1 is empty: no value was ever put there. In filled, every index holds a real 0.So a hole is not the same as a stored undefined. Reading scores[1] gives undefined, but nothing is stored there. Loops and the engine must handle that missing index in a special way. When an array holds repeated data, fill every index on purpose.
One call, one kind of object
A call site is one place in your code that calls a function, such as total(order). Each time that line runs, the engine makes a note of what it saw. If it keeps seeing the same thing, the engine can make that line faster.
In the code below, the engine keeps notes at two different spots:
- At the call
total(order)on line 8, it notes which function was called. Here it is alwaystotal. - At the property read
order.qtyon line 2, it notes the shape of the object. Here it is always "item, thenqty".
When a spot sees only one kind of thing, it is called monomorphic, which means "one form". Step through the code to see both spots at work.
A replay of real calls to total. The idea of one shape at one read comes from a simple teaching model of the engine.
script
function total(order) { return order.qty * 10;} const coffee = { item: "coffee", qty: 2 };for (const order of [tea, coffee]) { console.log(total(order));}The loop on lines 7 and 8 calls total(order) twice: first with tea, then with coffee. Both times, the function called is total. Both times, order.qty reads an object that has item then qty. The values differ (3 and 2), but the shape is the same. Tea gives 30 and coffee gives 20.
total(order), for tea and then for coffee. Both objects have item then qty, so order.qty sees the same layout each time.Two more words describe spots that see more variety:
- Polymorphic ("many forms"): the spot sees a few kinds, such as 2 to 4.
- Megamorphic ("huge number of forms"): the spot sees a lot of kinds.
At a call, a new kind means a different function. At a property read, a new kind means a different object shape. These words are clues for finding slow code. They are not a reason to remove useful code that works with many kinds of objects.
Playground: more shapes at one property read
At each property read, the engine keeps a small memory called an inline cache. It remembers the shapes it has seen there, and where qty sits in each one. The inline caches lesson explains it in full.
In this playground, change one number: how many different shapes reach the same order.qty read. The labels come from a simple teaching model. They do not come from your real browser.
function total(order) { return order.qty * 10;} const tea = { item: "tea", qty: 3 };const coffee = { item: "coffee", qty: 2 };for (const order of [tea, coffee]) { console.log(total(order));}1shape 1
In this model, a read that sees 1 shape is monomorphic. With 2 to 4 shapes, it is polymorphic and remembers each one. With 5 or more, it is megamorphic. The list above never holds more than 4 shapes.
In this teaching model, a read that sees 1 different shape is monomorphic.
Try 1, then 2, then 5. With 1 shape, the read is monomorphic. With 2 to 4 shapes, it is polymorphic. With 5 or more, it is megamorphic. Real engines choose their own limits. Reset goes back to 1 shape.
Avoid arguments and eval in hot code
Some functions take any number of inputs. For these, use rest parameters: three dots before a name, like ...prices. They collect all the inputs into a normal array.
The older way is the arguments object. Every regular function gets this special list of its inputs. It is harder to read, so don't make it your first choice in new hot code.
function addPrices(...prices) { return prices.reduce((sum, price) => sum + price, 0);} console.log(addPrices(10, 20, 30));- Line 1 collects every input into the array
prices. - Line 2 adds them up with
reduce, starting from 0. - Line 5 prints
60.
The code is easy to read, and it doesn't need the special arguments object.
eval runs a string as JavaScript code. That string can read or create local variables. The engine can't know ahead of time what the string will touch. So it has to run the whole function in a slower, more careful way. eval is also a safety risk: if the string comes from a user, the user can run their own code. Avoid it.
function priceWithEval(price) {
// Only for discussion: the string reads the local variable price.
return eval("price * 2");
}
console.log(priceWithEval(10));Line 3 runs the string "price * 2". The string reads the local variable price, so line 6 would print 20. Write price * 2 directly instead.
Proof from the V8 engine
V8 is the JavaScript engine inside Chrome and Node. Its inner workings are not part of JavaScript, and web pages can't see them. But Node can turn on special test functions with the flag --allow-natives-syntax. Their names start with %. This lesson's test uses three of them to check the claims above:
%HaveSameMap(a, b)answers: do these two objects share one shape? V8 calls a shape a map.%HasFastProperties(obj)answers: does this object still use the fast, shape-based layout?%HasHoleyElements(arr)answers: has V8 marked this array as one that may have holes?
class Order {
constructor(item, qty) {
this.item = item;
this.qty = qty;
}
}
const first = new Order("tea", 3);
const second = new Order("coffee", 2);
const a = {}; a.item = "tea"; a.qty = 3;
const b = {}; b.qty = 3; b.item = "tea";
const changed = { item: "tea", qty: 3, note: "hot" };
delete changed.qty;
console.log("same constructor map", %HaveSameMap(first, second));
console.log("different late maps", %HaveSameMap(a, b));
console.log("fast after delete", %HasFastProperties(changed));
console.log("holey", %HasHoleyElements([1, , 3]));
console.log("packed", %HasHoleyElements([1, 2, 3]));The test runs this code in Node with --allow-natives-syntax. It checks true or false answers, not timings. It found:
- The two orders made by the constructor share one map.
aandb, built in different orders, do not share a map.- After deleting
qty, which was not the last property, the object loses its fast layout. [1, , 3]is marked as holey, and[1, 2, 3]is not.
Use these test functions only to learn. Never put % functions in real app code. Other engines work in their own way, and V8 itself changes over time.
Habits for real code
Pick one place where data enters the hot code. At that place, clean up the data from the server once. For example, give every order the same fields, in the same order. Then pass these tidy orders into the repeated calculation. This is easier to test than adding special cases inside the loop.
The table below separates four ideas that sound alike. Each row says what the idea means, and what it does not mean.
| Idea | What it means | What it does not mean |
|---|---|---|
| Stable shape | Objects of one kind get the same property names, added in the same order. | It does not promise that a speed test will get faster. |
A property set to undefined | The property is still on the object. Its value is just empty. | It is not a deleted property. A deleted property is gone. |
| Packed array | Every index from 0 up to the last one holds a value. | It is not an array with holes in it. |
| Monomorphic | A call always runs the same function, or a property read always sees the same shape. | It does not mean one single object, used forever. |
Use the steps from measuring performance before and after one small change. A tiny speed test of one loop is called a microbenchmark. If it gets faster but no real user action gets faster, the change did not help.
- Use one constructor that writes
item, thenqty.It belongs in Keeps repeated work the same. Every new order gets its fields in the same order.
- Clear an optional note by setting it to
undefined. - Fill an array with
Array.from. - Send five differently built objects to one
order.qtyread. - Delete a middle property inside a busy loop.
- Record a profile of the slow action.
Put each card in a group: does it keep repeated work the same each time, does it add variety at a hot spot, or is it something to do before you change code?
Common misconceptions
- “Stable shapes make all code faster.” No. They only help where you have measured a slow, repeated piece of work.
- “You must never use delete.” No.
deleteis fine when you really mean "this property is gone". Just avoid it inside a hot loop you have measured. - “Monomorphic means every object is the same.” No. A call is monomorphic when it always calls the same function. A property read is monomorphic when the objects all have the same shape. Their values can all be different.
- “The V8 test functions are part of JavaScript.” No. They only exist in V8, behind a Node flag. This lesson uses them only as proof.
The table below lists four terms from this lesson. Each row says what the term means, and what people often mix it up with.
| Term | What it means | Not the same as |
|---|---|---|
| Fast path | A quicker way the engine prepares for work it has seen many times. | A promise made by the JavaScript language. |
| Shape | The engine's note about which property names an object has, and in what order. | Just the list that Object.keys prints. |
| Hot code | Code that runs so often that it shows up when you measure. | Every function in an app. |
| Measure first | Make sure a spot is really slow before you change clear code. | A rule against simple habits, like one constructor. |
Practice exercises
What does this code print? Type the exact output.
const order = { item: "tea", qty: 3 };
console.log(Object.keys(order).join(","));It prints item,qty. Those are the two property names, in the order they were added.
Does index 1 exist in this array? Type what the code prints.
const scores = [1, , 3];
console.log(1 in scores);It prints false, because index 1 is a hole. Nothing is stored there.
What number does this code print?
function total(order) { return order.qty * 10; }
console.log(total({ item: "tea", qty: 3 }));It prints 30. total reads qty, which is 3, and multiplies it by 10.
An order has a note. How would you clear the note without removing the property?
order.note = undefined;The order keeps its note property, but the note now has no value. The object keeps its shape.
On a shop website, the checkout total takes a long time to appear. What should you do before you rewrite the code?
Measure first. Record a profile while you click the slow checkout button. Then change only the code the profile shows is hot.
A new function takes any number of prices. Which kind of parameter is clearer than the arguments object?
function addPrices(...prices) {}Rest parameters show, right in the first line of the function, that it takes any number of inputs.
Check your understanding
Read each small piece of code slowly. For each question, first ask what it is about: what the code prints, the engine's teaching model, or when to measure.
Question 1 of 7When should these engine-friendly habits change your code?
Choose an answer to see the explanation.
Question 2 of 7What does this print?
Read the code, then predictconst order = { item: "tea", qty: 3 }; console.log(Object.keys(order).join(","));Choose an answer to see the explanation.
Question 3 of 7Why give an optional field a value in the constructor?
Choose an answer to see the explanation.
Question 4 of 7What does this code print?
Read the code, then predictconst scores = [1, , 3]; console.log(1 in scores);Choose an answer to see the explanation.
Question 5 of 7In this lesson, what makes the call
total(order)monomorphic?Choose an answer to see the explanation.
Question 6 of 7Why avoid eval in hot code?
Choose an answer to see the explanation.
Question 7 of 7A page feels slow. What should you do first?
Choose an answer to see the explanation.
Key takeaways
- Measure first. These habits belong only in hot code that you have measured.
- Build objects of one kind with the same properties, in the same order.
- Give optional fields a value, such as
undefined, at the start. Avoiddeletein hot loops. - Fill arrays fully. Don't leave holes in repeated data.
- Let each hot call or property read see only a few kinds of objects.
- Use rest parameters, and keep
evalout of hot code.
Remember the one-liner.
When repeated work looks the same each time, the engine can prepare a faster path for it.
Coming next: Looking inside the engine. You will use tools and flags to see what the engine decides, instead of guessing.