Skip to main content

Overview

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.

JIT compilation pipeline

Modern engines use a multi-tier approach that progressively optimizes hot code:
Ignition (V8’s interpreter)
  • Executes bytecode directly
  • Fast startup (no compilation delay)
  • Collects type feedback for later optimization
  • Low memory footprint
Compilation has cost. Engines carefully balance when to trigger JIT compilation based on function hotness (call frequency) and compilation cost.

Inline caching

Inline caching (IC) is the most important JIT optimization. It speeds up property access by caching location information from previous lookups.

Property access without IC

Without caching, every property access requires a full lookup:

Monomorphic inline cache

Monomorphic IC handles one object shape (hidden class):
Monomorphic IC can be 10-100x faster than prototype chain traversal by turning property access into a direct memory read at a fixed offset.

Polymorphic inline cache

Polymorphic IC handles 2-4 different shapes:

Megamorphic inline cache

When >4 shapes are seen, the IC becomes megamorphic (falls back to generic lookup):
Avoid megamorphic ICs:
  • Use consistent object shapes (same properties, same order)
  • Initialize all properties in constructor
  • Avoid dynamically adding/deleting properties
  • Use separate functions for different types

IC optimization strategies

Type feedback and speculative optimization

Engines collect type information during execution and use it to generate specialized, fast code.

Type feedback collection

Speculative optimization

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

Type specialization examples

Type-stable code can be 10-100x faster than type-unstable code. Keep function inputs and internal types consistent.

Deoptimization and bailout mechanisms

When assumptions are violated, optimized code must deoptimize back to the interpreter.

What triggers deoptimization

Deoptimization process

1

Guard failure

Type check or assumption fails in optimized code
2

State reconstruction

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 costs

Deoptimization is expensive:
  • State reconstruction overhead (100-1000 cycles)
  • Lost optimization (back to slow interpreter)
  • Potential re-optimization cost
  • Repeated deoptimization may prevent future optimization

Register allocation

Optimizing JIT compilers allocate CPU registers to variables for maximum performance.

Register vs memory access

Register allocation strategies

Characteristics:
  • Fast allocation algorithm
  • Single pass over code
  • Used in baseline JIT
  • May spill to stack under register pressure

Optimizing for register allocation

JIT optimization best practices

Advanced optimization techniques

Modern JIT compilers apply dozens of optimization passes. Understanding the most impactful ones helps you write faster code.

Performance monitoring

Next steps

Garbage collection

Learn memory management and GC optimization strategies

Event loop

Master asynchronous JavaScript execution and task scheduling