Writing engine-friendly code
Learn practical JavaScript habits for stable shapes, packed arrays, predictable call sites, and measured hot-path optimization.
- 01Keep shapes predictableBuild similar objects with the same fields in the same order and understand why that helps hot code.
- 02Recognize call-site varietyDescribe monomorphic, polymorphic, and megamorphic calls using a small inline-cache model.
- 03Optimize from evidenceMeasure a real hot path before applying engine-aware habits to production code.
Measure before changing code
Start with one rule: measure first. These habits matter in hot code, code that runs thousands of times in a measured slow path. Clear code wins everywhere else.
Engine-friendly code is clear JavaScript that keeps repeated, measured operations predictable enough for an engine to specialize them.
Engines already optimize ordinary code. You do not need to outguess them in every component. Use a profile, a user-visible slow interaction, and a small before-and-after check before accepting any extra constraint.
This practical lesson builds on the machinery in hidden classes, inline caches, elements kinds, type feedback, and speculation and deoptimization.
Stable object shapes
An object shape is an engine description of which properties an object has and how it got them. A simple, useful habit is adding the same properties in the same order for objects used in a hot path.
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 1 declares Order. Lines 2 through 4 build every order by writing item and then qty. Lines 10 and 11 print item,qty twice. The values differ, but the keys arrive in the same order.
In a home kitchen, finding a spoon is easy when spoons are always in the first drawer. If every family member uses a different drawer, you look around first. Repeated object layouts give the engine a similarly predictable pattern.
- In real life: Spoons always use the first drawer
- In JavaScript: Objects use one field order
- In real life: You grab a spoon without searching
- In JavaScript: The engine recognizes a common layout
- In real life: Different drawers require a search
- In JavaScript: Different layouts need more handling
Where the analogy stops: An engine has no kitchen drawers; it uses internal metadata and may choose different strategies.
Replay of instrumented constructor calls. It teaches a stable construction pattern; it is not an engine debugger.
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(","));Initialize every field
Optional does not need to mean late. Give an optional field a value such as undefined in the constructor when most hot-path objects should have that slot.
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 through 3 add item then qty to a. Lines 6 through 8 reverse that order for b. The output is item,qty and then qty,item. This visible difference is evidence for why one constructor is easier to keep consistent.
Do not add constructor ceremony to a one-off object. Use it where many objects feed repeated work. The goal is a simple construction rule, not a class-heavy style.
Avoid delete and holes
Deletion removes a property. It can force an object to change after the layout that hot code observed. If a field is optional, setting it to undefined keeps the property present.
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);Line 2 deletes note, while line 3 creates the property again with undefined. The keys print item,qty,note. Line 6 makes a hole at index 1, so line 8 prints false. The filled array has index 1, so line 9 prints true.
A hole is not the same as an explicit undefined. The array length can include both, but iteration and element storage have to account for the missing index. Fill arrays deliberately when they are repeated data.
Monomorphic call sites
A call site is one expression such as total(order). A monomorphic call site sees one receiver layout repeatedly. That gives an inline cache, a small remembered fast path, a simple case to specialize.
Replay of real calls to total. The shape labels are a teaching model of an inline cache.
script
function total(order) { return order.qty * 10;} const coffee = { item: "coffee", qty: 2 };console.log(total(tea));console.log(total(coffee));The replay calls the real total function with tea and coffee. Both objects have item then qty, so the model calls this one-shape input. Values do not need to match; the layout does.
Polymorphic means a small number of layouts appear at one call site. Megamorphic means many appear. Those labels are useful clues, not an instruction to remove every useful abstraction.
Inline cache playground
Change one input: the number of different layouts that reach the same call expression. The labels are a teaching model. They help you reason about why a common receiver shape is simpler than a parade of unrelated objects.
function total(order) { return order.qty * 10;} const tea = { item: "tea", qty: 3 };const coffee = { item: "coffee", qty: 2 };console.log(total(tea));console.log(total(coffee));1shape 1
A monomorphic site sees one layout. Two through four layouts are polymorphic in this lesson model. More layouts are megamorphic.
This teaching model labels 1 observed shape as monomorphic.
Reset returns to one shape. Try 1, then 2, then 5. The result changes from monomorphic to polymorphic to megamorphic without pretending to expose a particular browser's internal cache.
Avoid arguments and eval in hot paths
Prefer rest parameters when a function accepts a flexible list. They state the input in the parameter list and work naturally with array methods. The older arguments object is harder to reason about and should not be the default for new hot code.
function addPrices(...prices) { return prices.reduce((sum, price) => sum + price, 0);} console.log(addPrices(10, 20, 30));Line 1 gathers prices into an array. Line 2 adds them. Line 5 prints 60. This is clear source first; it also avoids relying on the special arguments object.
eval can execute text that reads or creates local names. That blocks useful assumptions because the engine cannot know in advance what the string will touch. Avoid it for correctness and security too.
function priceWithEval(price) {
// Node-only discussion example: eval can read local names dynamically.
return eval("price * 2");
}
console.log(priceWithEval(10));Small V8 evidence
Engine internals are not browser APIs. For a stable lesson probe, Node can enable V8 native syntax in a child process. The test asserts booleans, not timings or a Node patch version.
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 with --allow-natives-syntax. Two constructor-built orders have the same map. Different late-field orders do not. Deleting a non-last field loses fast properties, and the holey array is reported as holey while [1, 2, 3] is not.
Use these probes to learn the idea. Do not ship percent-prefixed V8 intrinsics in application code. Engines differ, and their implementation details change.
Practical habits
Keep a narrow boundary where data becomes hot. Normalize API data once, then pass a small predictable order object through a repeated calculation. This is easier to test than scattering special cases inside the loop.
| Idea | Meaning | Do not promise |
|---|---|---|
| Stable shape | Same property names added in the same order | A promise that a benchmark will be faster |
| Undefined slot | A present property whose value is undefined | A deleted property that no longer exists |
| Packed array | Every index from zero to length minus one exists | An array with empty holes |
| Monomorphic call | One receiver shape reaches one call site | One object instance forever |
Use measuring performance before and after a focused change. A faster microbenchmark that does not improve a user flow is not the result you need.
- Use one constructor that writes item then qty.
It belongs in Keeps common code predictable. Every new Order gets fields in one order.
- Clear optional note with
undefined. - Fill an array with
Array.from. - Pass five unrelated layouts to one
totalcall. - Delete a middle property in a tight loop.
- Record a profile around the slow interaction.
Sort each card by whether it keeps a hot pattern predictable, adds variety, or should happen before a rewrite.
Common misconceptions
- “Stable shapes make all code faster.” They only matter when a measured repeated operation benefits.
- “Delete is forbidden.” It is useful for real semantics; avoid structural churn in proven hot loops.
- “Monomorphic means identical objects.” Values may differ while property layout remains alike.
- “A V8 probe is a web standard.” It is engine-specific evidence for this lesson.
| Term | Meaning | Not the same as |
|---|---|---|
| Fast path | An engine specialization for observed common behavior | A JavaScript language guarantee |
| Shape | An engine's property-layout description | The exact visible Object.keys output alone |
| Hot code | Code that runs enough to matter in measurement | Every function in an app |
| Measure first | Confirm a bottleneck before changing readable code | Never use simple construction patterns |
Practice exercises
Type the visible output.
const order = { item: "tea", qty: 3 };
console.log(Object.keys(order).join(","));It prints item,qty; those are the two inserted keys in order.
Predict whether the middle index exists.
const scores = [1, , 3];
console.log(1 in scores);It prints false because index 1 does not exist.
Type the number printed by the hot-call example.
function total(order) { return order.qty * 10; }
console.log(total({ item: "tea", qty: 3 }));It prints 30. The object has the field total reads.
How would you clear an optional note without removing the property?
order.note = undefined;This keeps the note slot while expressing no current note.
A checkout calculation feels slow. What should happen before a rewrite?
Measure first: record a profile around the slow checkout action, then change only the proven hot path.
Which parameter syntax is clearer than arguments for a new function?
function addPrices(...prices) {}Rest parameters show flexible inputs directly in the signature.
Check your understanding
Read each small snippet slowly. Then separate a source pattern, an engine model, and the practical decision to measure.
Question 1 of 7When should 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 initialize an optional field in a constructor?
Choose an answer to see the explanation.
Question 4 of 7What does
1 in [1, , 3]print?Read the code, then predictconst scores = [1, , 3]; console.log(1 in scores);Choose an answer to see the explanation.
Question 5 of 7What is a monomorphic call site in this lesson's model?
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 7Which action is the practical starting point?
Choose an answer to see the explanation.
Key takeaways
- Measure first; these habits belong in proven hot code.
- Build repeated objects with the same fields in the same order.
- Initialize optional fields and avoid structural delete churn where it matters.
- Keep repeated call sites focused on a small set of layouts.
- Use rest parameters and keep eval out of hot code.
Remember the one-liner.
Predictable repeated data gives an engine a simpler path to specialize.
Coming next: Looking inside the engine. You will use flags and tools to see optimization decisions instead of guessing.
Field notes for a measured hot path
Read shapes as a construction story
Object keys are useful visible evidence, but they are not an engine map. They show the construction story a reader can check: first item, then qty, then an optional note. When many orders follow that story, a runtime has a simple repeated case to observe.
There is no need to force unrelated data into one shape. A search result and a checkout order serve different jobs. The useful boundary is narrower: objects that flow through one measured calculation should normally be created by one small factory or constructor.
When a profile points at property reads, log a few representative inputs before changing source. Look for accidental variation such as a field added in one event handler, a default applied only on one fetch path, or a literal with keys written in a different order. Fix the source of that variation once rather than adding conditions to every read.
Give optional fields an honest initial value
An optional field has two separate questions: does this object have a place for the field, and does it currently have a value? Initializing note to null answers the first question yes and the second question no. Adding note later combines both questions and makes object construction less obvious.
Choose null when your application uses it to mean intentionally empty. Choose undefined when absence is the established local convention. The shape lesson does not choose that semantic policy for you; it only says that a repeated representation is easier to understand when the policy is applied at construction.
This also improves tests. A test can assert the complete order contract immediately after construction instead of needing a second test for a later mutation. If the field truly belongs only to one rare feature, split that rare data into a separate object rather than filling every common object with confusing fields.
Remove values without making accidental holes
Setting order.note = undefined says the note has no value now. Deleting it says the order no longer has a note property at all. Both are valid language operations, but they are different data models and should not be swapped merely because one looks shorter.
Array holes deserve the same care. In [1, , 3], index 1 is absent. Reading scores[1] produces undefined, which can hide the difference, while 1 in scores exposes it. A reducer, iterator, or copied list can also treat a missing slot differently from an explicit value.
Use Array.from({ length: 3 }, () => 0) when a repeated list should begin with three real zeroes. Use new Array(3) only when you intentionally need an empty sparse container and will fill it carefully. This is first a correctness and clarity choice; the engine-friendly benefit is a useful extra in a measured loop.
Follow one call expression, not a whole function name
A function can be called from several places. One site may always pass normalized orders, while another passes raw API objects. The engine's feedback is attached to each call expression, so inspect the call site that appears in the profile instead of saying that the whole function is monomorphic or megamorphic.
The inline-cache model has four moments. The first call records a shape. A matching call is a hit. A new shape makes the site polymorphic. Enough different shapes make the model megamorphic. Real engines use their own thresholds and strategies, so use those words to guide investigation, not to predict a benchmark result.
A practical repair is often to normalize values before the hot call. Keep API-specific cleanup at the boundary, then call total(order) with one documented order contract. That makes the loop simpler for readers and may reduce accidental shape variety without changing the public API.
Keep scope and parameters visible
Rest parameters put flexible input at the function boundary: function addPrices(...prices). A reader immediately knows that prices is an array-like list owned by this call. The function can reduce it without reaching for the older special arguments object.
Eval does the opposite of visible scope. A string can refer to local names that do not appear as normal identifier reads in the surrounding source. That makes static reasoning, security review, and engine optimization harder at the same time. Replace it with direct expressions, data lookup, or a small explicit parser.
Do not turn this into a hunt for forbidden syntax. A legacy function using arguments may be perfectly fine outside a measured path. The reliable sequence remains the same: identify a real slow interaction, inspect inputs and call sites, make one small clarity-preserving change, then measure again.
A small review checklist
Before changing a hot function, write down the interaction, the profile evidence, and the call expression you are changing. State the input contract in one sentence. For an order total, that might be: every order has item, qty, price, and note, and qty and price are numbers.
Next, make the smallest local change. Normalize raw values once, initialize the fields in one order, or replace a sparse initializer with a filled one. Avoid a broad refactor that changes unrelated behavior, because a benchmark result is less useful when it is mixed with many semantic changes.
Finally, rerun the same interaction and keep ordinary correctness tests. Compare user-facing work, not only a synthetic loop. If the profile does not improve, keep the clearer version or investigate a different bottleneck. Engine-friendly code is evidence-led maintenance, not a collection of magic patterns.