Just-in-time (JIT) compilation transforms JavaScript from interpreted bytecode into optimized machine code at runtime. Modern JavaScript engines use sophisticated profiling and optimization techniques to identify and accelerate hot code paths, achieving near-native performance.
JIT compilation is optional in engine implementation but critical for production performance. Engines like V8, SpiderMonkey, and JavaScriptCore use multi-tier JIT systems that balance compilation cost with execution speed.
// After a few calls, compiles to baselinefor (let i = 0; i < 100; i++) { add(i, i + 1); // Baseline JIT after ~10 iterations}
TurboFan (V8’s optimizing compiler)
Expensive compilation process
Aggressive optimizations using type feedback
Speculative optimization with guards
10-100x faster than interpreter for numeric code
// After many calls with consistent typesfor (let i = 0; i < 10000; i++) { add(i, i + 1); // Optimizing JIT after ~1000 iterations}// Optimized version assumes integers// Uses fast integer addition instruction
Compilation has cost. Engines carefully balance when to trigger JIT compilation based on function hotness (call frequency) and compilation cost.
When >4 shapes are seen, the IC becomes megamorphic (falls back to generic lookup):
function getName(obj) { return obj.name;}// Called with many different shapesfor (let i = 0; i < 10; i++) { const obj = {}; obj[`prop${i}`] = i; // Different shape each time! obj.name = `name${i}`; getName(obj); // IC becomes megamorphic after 4-5 different shapes}// Performance cliff: 10x slower than monomorphic
Avoid megamorphic ICs:
Use consistent object shapes (same properties, same order)
function add(a, b) { return a + b;}// Interpreter tracks types seen:add(1, 2); // Feedback: int + int -> intadd(3, 4); // Feedback: int + int -> intadd(5, 6); // Feedback: int + int -> int// After profiling, optimizing JIT generates:// Specialized integer addition (single CPU instruction)// Guards check inputs are integers
The JIT compiler makes assumptions based on profiled types and inserts guards:
1
Collect feedback
Interpreter observes types during execution
2
Speculate
Assume types will remain consistent
3
Optimize
Generate specialized fast code
4
Guard
Insert type checks at function entry
5
Deoptimize
If guards fail, fall back to interpreter
function add(a, b) { return a + b;}// Optimized version (pseudocode):function add_optimized(a, b) { // Guard: Check assumptions if (typeof a !== 'number' || typeof b !== 'number') { deoptimize(); // Bailout to interpreter return add_unoptimized(a, b); } // Fast path: Integer addition return int_add(a, b); // Single CPU instruction}
function calculate(x) { return x * 2 + 1;}// Integer feedbackfor (let i = 0; i < 10000; i++) { calculate(i); // JIT sees only integers}// Optimized to:// int_multiply(x, 2) + 1// (2-3 CPU instructions)// Deoptimizes if called with:calculate(3.14); // Floatcalculate("5"); // Stringcalculate(BigInt(5)); // BigInt
function sum(arr) { let total = 0; for (let i = 0; i < arr.length; i++) { total += arr[i]; } return total;}// Feedback: Array of integersconst nums = [1, 2, 3, 4, 5];sum(nums);// Optimized version uses:// - Packed integer array fast path// - Bounds check elimination (length is constant)// - Integer addition without type checks// Deoptimizes if:sum([1, 2, 3.5]); // Becomes double arraysum([1, 2, "3"]); // Becomes generic arraysum([1, 2, , 4]); // Holey array (sparse)
function getCoordinates(point) { return { x: point.x, y: point.y };}const p1 = { x: 10, y: 20 };const p2 = { x: 30, y: 40 };// Feedback: Monomorphic shapegetCoordinates(p1);getCoordinates(p2);// Optimized version:// - Direct memory offsets (no property lookup)// - Inline object allocation// - Eliminates temporary object if unused// Deoptimizes if called with:getCoordinates({ y: 1, x: 2 }); // Different shape (property order)getCoordinates({ x: 1, y: 2, z: 3 }); // Different shape (extra property)
Type-stable code can be 10-100x faster than type-unstable code. Keep function inputs and internal types consistent.
Reconstruct interpreter state from optimized frame
3
Bailout
Jump back to interpreter at correct position
4
Continue execution
Interpreter continues with correct state
5
Remark for optimization
Function may be re-optimized with new feedback
// Deoptimization examplefunction add(a, b) { return a + b; // Line 2}// Optimized for integersadd(1, 2);add(2, 3);// Deoptimization:add("hello", "world");// Engine:// 1. Detects type mismatch in optimized code// 2. Reconstructs interpreter state:// - PC: line 2// - Variables: a="hello", b="world"// 3. Jumps to interpreter// 4. Interpreter completes string concatenation// 5. Function marked for re-optimization with new type info
Repeated deoptimization may prevent future optimization
Soft deopt
Hard deopt
Bailout prevention
// Soft deopt: Rare path, can re-optimizefunction process(x) { if (typeof x === 'number') { return x * 2; // Hot path (optimized) } return String(x); // Cold path (rare)}// 99.9% called with numbersfor (let i = 0; i < 10000; i++) { process(i);}// Occasional string doesn't break optimizationprocess("hello"); // Soft deopt, then continues
// Hard deopt: Permanent assumption violationfunction calculate(x) { return x * x;}// Initially called with small integersfor (let i = 0; i < 10000; i++) { calculate(i); // Optimized for small ints}// Large number changes assumptionscalculate(Number.MAX_SAFE_INTEGER); // Hard deopt// Future calls may not re-optimize
// Strategy 1: Type guards in sourcefunction safeMath(x) { if (typeof x !== 'number') { return 0; // Early exit for non-numbers } return x * x; // Can stay optimized}// Strategy 2: Separate code pathsfunction processNumber(x) { return x * 2;}function processString(x) { return x.repeat(2);}function process(x) { if (typeof x === 'number') { return processNumber(x); // Stays monomorphic } return processString(x); // Separate optimization}
// Linear scan allocator (fast compilation)function calculate(a, b, c) { const x = a + b; // x -> register r1 const y = b + c; // y -> register r2 const z = x * y; // z -> register r3 return z;}// Assembly-like output:// r1 = add r_a, r_b// r2 = add r_b, r_c// r3 = mul r1, r2// return r3
Characteristics:
Fast allocation algorithm
Single pass over code
Used in baseline JIT
May spill to stack under register pressure
// Graph coloring (optimizing compiler)function complex(a, b, c, d) { const w = a + b; const x = b + c; const y = c + d; const z = w + x + y; return z;}// Interference graph:// w ----- x// \ / \// \ / y// \ / /// z /// \ /// result// Coloring (register assignment):// w -> r1, x -> r2, y -> r1 (w is dead), z -> r3
Characteristics:
Optimal register usage
Considers variable lifetime
Used in optimizing JIT
Expensive to compute
// High register pressure examplefunction manyVariables() { const a = 1, b = 2, c = 3, d = 4; const e = 5, f = 6, g = 7, h = 8; const i = 9, j = 10, k = 11, l = 12; // x64 has ~16 general-purpose registers // Some reserved for special purposes // Realistically ~8-10 available return a+b+c+d+e+f+g+h+i+j+k+l; // May spill to stack}// Optimization: Reduce live rangesfunction optimized() { let sum = 0; sum += 1 + 2; // r1 = 3 sum += 3 + 4; // r1 += 7 sum += 5 + 6; // r1 += 11 // ... only need 1-2 registers return sum;}
// BAD: Long variable lifetimesfunction processData(data) { const temp1 = data.map(x => x * 2); const temp2 = data.map(x => x + 1); const temp3 = data.map(x => x - 1); // All temps live simultaneously - register pressure! return temp1.concat(temp2).concat(temp3);}// GOOD: Short variable lifetimesfunction processData(data) { let result = []; result = result.concat(data.map(x => x * 2)); // temp1 dies result = result.concat(data.map(x => x + 1)); // temp2 dies result = result.concat(data.map(x => x - 1)); // temp3 dies return result;}// BETTER: Eliminate temporariesfunction processData(data) { return data.flatMap(x => [x * 2, x + 1, x - 1]);}
// 1. Use consistent typesfunction add(a, b) { return a + b; // Keep inputs same type}// 2. Avoid polymorphismclass Point { constructor(x, y) { this.x = x; // Always initialize all properties this.y = y; // in same order }}// 3. Use monomorphic call sitesfunction process(obj) { return obj.value; // Call with same object shape}// 4. Prefer arrays over argumentsfunction sum(...args) { // Good: real array return args.reduce((a, b) => a + b, 0);}// 5. Avoid eval and with// (prevents many optimizations)
// Use performance.now() for timingfunction benchmark(fn, iterations = 10000) { // Warmup: Allow JIT to optimize for (let i = 0; i < 1000; i++) { fn(); } // Measure const start = performance.now(); for (let i = 0; i < iterations; i++) { fn(); } const end = performance.now(); return (end - start) / iterations;}// Check optimization status (V8)function checkOptimization(fn) { %PrepareFunctionForOptimization(fn); fn(); // Run once %OptimizeFunctionOnNextCall(fn); fn(); // Triggers optimization // Run with: node --allow-natives-syntax script.js}
// V8 flags for debugging JIT// node --trace-opt --trace-deopt script.jsfunction problematic(x) { return x * 2;}// Warm upfor (let i = 0; i < 10000; i++) { problematic(i);}// Output: [optimized problematic]// Trigger deoptproblematic("oops");// Output: [deoptimized problematic because wrong type]// Use chrome://tracing for detailed profiles// Or Chrome DevTools Performance tab
// Before inliningfunction add(a, b) { return a + b;}function calculate(x) { return add(x, 5) * 2;}// After inlining (by JIT)function calculate_optimized(x) { // add() body inlined return (x + 5) * 2; // Benefits: // - No function call overhead // - More optimization opportunities // - Better register allocation}// Inlining heuristics:// - Small functions (<600 bytecode bytes)// - Monomorphic call sites// - Not too deep nesting
// Object allocation eliminationfunction calculate(x, y) { const point = { x, y }; // Allocated on heap normally return point.x + point.y;}// Optimized version (escape analysis):function calculate_optimized(x, y) { // Object doesn't escape function // JIT eliminates allocation entirely return x + y; // Direct computation}// Object escapes - cannot optimize:function noOptimize(x, y) { const point = { x, y }; return point; // Escapes! Must allocate}
// Loop invariant code motionfunction sumWithConstant(arr, multiplier) { let sum = 0; for (let i = 0; i < arr.length; i++) { sum += arr[i] * multiplier; // multiplier is loop invariant } return sum;}// JIT hoists invariants:function sumWithConstant_optimized(arr, multiplier) { let sum = 0; const m = multiplier; // Hoisted for (let i = 0; i < arr.length; i++) { sum += arr[i] * m; } return sum;}// Strength reductionfor (let i = 0; i < n; i++) { arr[i] = i * 4; // Multiplication}// Optimized to:let temp = 0;for (let i = 0; i < n; i++) { arr[i] = temp; // Addition (faster) temp += 4;}
// Bounds checks in loopsfunction sumArray(arr) { let sum = 0; for (let i = 0; i < arr.length; i++) { sum += arr[i]; // Bounds check: i < arr.length? } return sum;}// JIT proves bounds check is redundant:function sumArray_optimized(arr) { let sum = 0; const len = arr.length; for (let i = 0; i < len; i++) { sum += arr[i]; // No bounds check needed! // Loop condition guarantees i < len } return sum;}// Enables SIMD and vectorization
Modern JIT compilers apply dozens of optimization passes. Understanding the most impactful ones helps you write faster code.