A comprehensive guide to writing JavaScript code that's friendly to JIT (Just-In-Time) compilers like V8, SpiderMonkey, and JavaScriptCore.
- Type Consistency
- Object Shape Stability
- Function Optimization
- Array Operations
- Number Operations
- Control Flow
function add(a, b) {
return a + b;
}
// Always call with the same types
add(1, 2);
add(5, 10);
add(100, 200);function add(a, b) {
return a + b;
}
// Called with different types - causes deoptimization
add(1, 2); // numbers
add("hello", "world"); // strings
add(1, "2"); // mixed typesESLint Rule: No existing rule covers this completely, but you can create a custom rule:
// eslint-plugin-jit-friendly/no-polymorphic-calls.js
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "Detect functions called with inconsistent argument types",
category: "Performance"
}
},
create(context) {
const functionCalls = new Map();
return {
CallExpression(node) {
const funcName = node.callee.name;
if (!funcName) return;
const argTypes = node.arguments.map(arg => arg.type);
if (!functionCalls.has(funcName)) {
functionCalls.set(funcName, argTypes);
} else {
const previousTypes = functionCalls.get(funcName);
if (JSON.stringify(previousTypes) !== JSON.stringify(argTypes)) {
context.report({
node,
message: `Function '${funcName}' called with inconsistent types`
});
}
}
}
};
}
};class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
const p1 = new Point(10, 20);
const p2 = new Point(30, 40);
// Both objects have the same hidden class/shapeconst obj1 = { x: 10 };
obj1.y = 20; // Shape change!
const obj2 = { x: 30 };
obj2.z = 40; // Different shape!
// Even worse: adding properties in different orders
const obj3 = {};
obj3.y = 20;
obj3.x = 10; // Different hidden class than { x: 10, y: 20 }ESLint Rule: no-param-reassign (partial coverage) or custom rule:
// eslint-plugin-jit-friendly/no-dynamic-property-addition.js
module.exports = {
meta: {
type: "problem",
docs: {
description: "Disallow adding properties after object creation",
category: "Performance"
}
},
create(context) {
const declaredObjects = new Map();
return {
VariableDeclarator(node) {
if (node.init && node.init.type === 'ObjectExpression') {
const props = node.init.properties.map(p => p.key.name);
declaredObjects.set(node.id.name, new Set(props));
}
},
AssignmentExpression(node) {
if (node.left.type === 'MemberExpression') {
const objName = node.left.object.name;
const propName = node.left.property.name;
if (declaredObjects.has(objName)) {
const knownProps = declaredObjects.get(objName);
if (!knownProps.has(propName)) {
context.report({
node,
message: `Dynamic property '${propName}' added to object '${objName}'`
});
}
}
}
}
};
}
};function processUser(user) {
return user.name + " " + user.email;
}
// All objects have the same shape
processUser({ name: "Alice", email: "alice@example.com" });
processUser({ name: "Bob", email: "bob@example.com" });function process(obj) {
return obj.value;
}
// Called with many different object shapes
process({ value: 1, type: "number" });
process({ value: "hello", id: 123 });
process({ value: true, metadata: {} });
process({ value: [], extra: "data" });
process({ value: {}, another: "field" });
// After ~4-5 different shapes, becomes megamorphicESLint Rule: No standard rule exists. Custom rule needed:
// This is complex to detect statically, but you can warn about
// functions that operate on parameters with property access
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "Warn about potential megamorphic property access"
}
},
create(context) {
return {
FunctionDeclaration(node) {
const memberAccesses = [];
// Traverse function body looking for parameter property access
// This is a simplified version
context.getDeclaredVariables(node).forEach(variable => {
const references = variable.references;
references.forEach(ref => {
if (ref.identifier.parent.type === 'MemberExpression') {
memberAccesses.push(ref.identifier.parent);
}
});
});
if (memberAccesses.length > 3) {
context.report({
node,
message: "Function may be megamorphic due to many property accesses"
});
}
}
};
}
};function calculateTotal(price, quantity) {
return price * quantity;
}
function applyDiscount(total, discount) {
return total * (1 - discount);
}
const total = calculateTotal(100, 5);
const finalPrice = applyDiscount(total, 0.1);function processOrder(order) {
// 500+ lines of code
// Complex logic with many branches
// Too large to inline
// May not get optimized at all
let total = 0;
for (let i = 0; i < order.items.length; i++) {
// ... 50 lines
}
if (order.discount) {
// ... 100 lines
}
// ... 350 more lines
return total;
}ESLint Rule: max-lines-per-function or complexity
{
"rules": {
"max-lines-per-function": ["error", {
"max": 50,
"skipBlankLines": true,
"skipComments": true
}],
"complexity": ["error", 10]
}
}const numbers = [1, 2, 3, 4, 5];
const strings = ["a", "b", "c"];
// Array is stored as packed SMI (small integer) array
numbers.forEach(n => console.log(n * 2));const mixed = [1, "two", { three: 3 }, null, undefined];
// Forces array to be stored as generic object array
// Much slower iteration and accessESLint Rule: array-element-newline doesn't cover this, custom rule needed:
module.exports = {
meta: {
type: "problem",
docs: {
description: "Disallow mixed-type arrays"
}
},
create(context) {
return {
ArrayExpression(node) {
if (node.elements.length === 0) return;
const types = new Set();
node.elements.forEach(element => {
if (element) {
types.add(element.type);
}
});
if (types.size > 1) {
context.report({
node,
message: "Array contains mixed types, which can harm performance"
});
}
}
};
}
};const arr = new Array(5).fill(0);
// or
const arr = [0, 0, 0, 0, 0];const arr = new Array(100);
arr[0] = 1;
arr[99] = 2;
// Creates holey array - much slower than packed arraysESLint Rule: no-sparse-arrays
{
"rules": {
"no-sparse-arrays": "error"
}
}const size = 1000;
const arr = new Array(size);
for (let i = 0; i < size; i++) {
arr[i] = i * 2;
}const arr = [];
for (let i = 0; i < 1000; i++) {
arr.push(i * 2); // Multiple reallocations
}ESLint Rule: No standard rule. Custom rule possible but complex.
function calculateIndex(row, col, width) {
return row * width + col; // SMI operations - very fast
}
// Bitwise operations are also fast
const rounded = (value + 0.5) | 0;function calculateIndex(row, col, width) {
return row * width + col + 0.1 - 0.1; // Forces float representation
}
const x = 5 / 2.0; // Creates float instead of integer divisionESLint Rule: No standard rule, custom needed:
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "Prefer integer operations over floating point"
}
},
create(context) {
return {
BinaryExpression(node) {
if (['+', '-', '*'].includes(node.operator)) {
const checkFloat = (n) => {
return n.type === 'Literal' &&
typeof n.value === 'number' &&
!Number.isInteger(n.value);
};
if (checkFloat(node.left) || checkFloat(node.right)) {
context.report({
node,
message: "Consider using integer operations for better performance"
});
}
}
}
};
}
};let count = 0; // SMI
count += 1;
count += 2;
count += 3;
// Stays as SMIlet count = 0; // SMI
count += 1;
count += 0.5; // Converted to float!
count += 2; // Now all operations are float operationsESLint Rule: Part of type consistency checking (custom rule needed).
function processValue(value) {
if (value > 0) {
return value * 2;
} else {
return value * 3;
}
}
// Called consistently
processValue(5); // true branch
processValue(10); // true branch
processValue(15); // true branchfunction processValue(value) {
if (Math.random() > 0.5) {
return value * 2;
} else {
return value * 3;
}
}
// Branch prediction fails constantlyESLint Rule: no-constant-condition (partial), custom for Math.random detection:
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "Warn about random values in conditionals"
}
},
create(context) {
return {
IfStatement(node) {
const test = node.test;
// Check if Math.random is used in condition
function hasRandom(node) {
if (node.type === 'CallExpression' &&
node.callee.type === 'MemberExpression' &&
node.callee.object.name === 'Math' &&
node.callee.property.name === 'random') {
return true;
}
return false;
}
if (hasRandom(test)) {
context.report({
node,
message: "Random values in conditions harm branch prediction"
});
}
}
};
}
};// Put try-catch at higher level
function main() {
try {
processData(data);
} catch (e) {
handleError(e);
}
}
function processData(data) {
// Hot path - no try-catch here
return data.map(x => x * 2);
}function processItem(item) {
try {
return item.value * 2; // Prevents optimization
} catch (e) {
return 0;
}
}
// Called thousands of times
items.map(processItem);ESLint Rule: No standard rule, custom needed:
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "Discourage try-catch in loops and hot paths"
}
},
create(context) {
return {
TryStatement(node) {
let parent = node.parent;
while (parent) {
if (['ForStatement', 'WhileStatement', 'DoWhileStatement',
'ForOfStatement', 'ForInStatement'].includes(parent.type)) {
context.report({
node,
message: "Avoid try-catch in loops - move to outer scope"
});
break;
}
parent = parent.parent;
}
}
};
}
};class User {
constructor(name, email) {
this.name = name;
this.email = email;
this.age = null; // Declare all properties upfront
}
}const obj = { x: 1, y: 2, z: 3 };
delete obj.y; // Changes hidden class, prevents optimization
// Use undefined instead
obj.y = undefined; // Better, but still not idealESLint Rule: no-delete-var (partial) or custom:
module.exports = {
meta: {
type: "problem",
docs: {
description: "Disallow delete operator on object properties"
}
},
create(context) {
return {
UnaryExpression(node) {
if (node.operator === 'delete' &&
node.argument.type === 'MemberExpression') {
context.report({
node,
message: "Avoid delete operator - set to undefined or redesign structure"
});
}
}
};
}
};const point = { x: 10, y: 20 };
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}function sum() {
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i]; // Prevents optimization
}
return total;
}
// Use rest parameters instead
function sum(...numbers) {
return numbers.reduce((a, b) => a + b, 0);
}ESLint Rule: prefer-rest-params
{
"rules": {
"prefer-rest-params": "error"
}
}Key Principles for JIT-Friendly Code:
- Monomorphic is better than polymorphic - Keep types consistent
- Stable object shapes - Don't add/delete properties dynamically
- Homogeneous arrays - One type per array
- Avoid holes - Keep arrays packed
- Integer math - Prefer SMIs over floats when possible
- Small functions - Easier to inline and optimize
- Predictable control flow - Help branch prediction
- Minimal try-catch - Don't use in hot paths
- Avoid
delete- It changes hidden classes - Use modern syntax - Rest params instead of
arguments
Testing Your Code:
You can use V8's built-in flags to check optimization status:
node --trace-opt --trace-deopt your-script.jsThis will show you which functions get optimized and which get deoptimized, helping you identify problem areas.