Skip to content

Instantly share code, notes, and snippets.

@dmitryweiner
Created January 12, 2026 07:37
Show Gist options
  • Select an option

  • Save dmitryweiner/5a2adc61c68f846038f3c7537ea1d8db to your computer and use it in GitHub Desktop.

Select an option

Save dmitryweiner/5a2adc61c68f846038f3c7537ea1d8db to your computer and use it in GitHub Desktop.
JavaScript JIT Optimization Guide

JavaScript JIT Optimization Guide

A comprehensive guide to writing JavaScript code that's friendly to JIT (Just-In-Time) compilers like V8, SpiderMonkey, and JavaScriptCore.

Table of Contents

  1. Type Consistency
  2. Object Shape Stability
  3. Function Optimization
  4. Array Operations
  5. Number Operations
  6. Control Flow

Type Consistency

✅ Good: Consistent Types

function add(a, b) {
  return a + b;
}

// Always call with the same types
add(1, 2);
add(5, 10);
add(100, 200);

❌ Bad: Polymorphic Functions

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 types

ESLint 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`
            });
          }
        }
      }
    };
  }
};

Object Shape Stability

✅ Good: Consistent Object Shape

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/shape

❌ Bad: Dynamic Property Addition

const 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 Optimization

✅ Good: Monomorphic Functions

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" });

❌ Bad: Megamorphic Functions

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 megamorphic

ESLint 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"
          });
        }
      }
    };
  }
};

✅ Good: Small Functions

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);

❌ Bad: Very Large Functions

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]
  }
}

Array Operations

✅ Good: Homogeneous Arrays

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));

❌ Bad: Heterogeneous Arrays

const mixed = [1, "two", { three: 3 }, null, undefined];
// Forces array to be stored as generic object array
// Much slower iteration and access

ESLint 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"
          });
        }
      }
    };
  }
};

✅ Good: Avoid Holes in Arrays

const arr = new Array(5).fill(0);
// or
const arr = [0, 0, 0, 0, 0];

❌ Bad: Sparse Arrays

const arr = new Array(100);
arr[0] = 1;
arr[99] = 2;
// Creates holey array - much slower than packed arrays

ESLint Rule: no-sparse-arrays

{
  "rules": {
    "no-sparse-arrays": "error"
  }
}

✅ Good: Pre-allocate Arrays When Size is Known

const size = 1000;
const arr = new Array(size);
for (let i = 0; i < size; i++) {
  arr[i] = i * 2;
}

❌ Bad: Growing Arrays Dynamically

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.


Number Operations

✅ Good: Use Integer Math When Possible

function calculateIndex(row, col, width) {
  return row * width + col; // SMI operations - very fast
}

// Bitwise operations are also fast
const rounded = (value + 0.5) | 0;

❌ Bad: Unnecessary Floating Point

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 division

ESLint 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"
            });
          }
        }
      }
    };
  }
};

✅ Good: Avoid Mixing Number Types

let count = 0; // SMI
count += 1;
count += 2;
count += 3;
// Stays as SMI

❌ Bad: SMI to Float Conversion

let count = 0; // SMI
count += 1;
count += 0.5; // Converted to float!
count += 2;   // Now all operations are float operations

ESLint Rule: Part of type consistency checking (custom rule needed).


Control Flow

✅ Good: Predictable Branches

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 branch

❌ Bad: Unpredictable Branches

function processValue(value) {
  if (Math.random() > 0.5) {
    return value * 2;
  } else {
    return value * 3;
  }
}

// Branch prediction fails constantly

ESLint 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"
          });
        }
      }
    };
  }
};

✅ Good: Use Try-Catch Sparingly

// 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);
}

❌ Bad: Try-Catch in Hot Paths

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;
        }
      }
    };
  }
};

Additional Anti-Patterns

✅ Good: Delete Properties at Creation

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
    this.age = null; // Declare all properties upfront
  }
}

❌ Bad: Using delete Operator

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 ideal

ESLint 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"
          });
        }
      }
    };
  }
};

✅ Good: Use Object Literals or Classes

const point = { x: 10, y: 20 };

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

❌ Bad: Using arguments Object

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"
  }
}

Summary

Key Principles for JIT-Friendly Code:

  1. Monomorphic is better than polymorphic - Keep types consistent
  2. Stable object shapes - Don't add/delete properties dynamically
  3. Homogeneous arrays - One type per array
  4. Avoid holes - Keep arrays packed
  5. Integer math - Prefer SMIs over floats when possible
  6. Small functions - Easier to inline and optimize
  7. Predictable control flow - Help branch prediction
  8. Minimal try-catch - Don't use in hot paths
  9. Avoid delete - It changes hidden classes
  10. 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.js

This will show you which functions get optimized and which get deoptimized, helping you identify problem areas.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment