# How JavaScript Engines Optimize Code with JIT Compilation, Inline Caching, and Hidden Classes

> Discover how JavaScript engines optimize code with JIT compilation, inline caching, and hidden classes for near-native speed. Learn the core concepts.

- Repository: [Leonardo Maldonado/33-js-concepts](https://github.com/leonardomso/33-js-concepts)
- Tags: internals
- Published: 2026-03-04

---

**Modern JavaScript engines combine Just-In-Time (JIT) compilation, hidden classes, and inline caching to transform dynamic JavaScript into optimized machine code that runs at near-native speed.**

The leonardomso/33-js-concepts repository documents how engines like V8, SpiderMonkey, and JavaScriptCore convert source text into fast executable code. These optimization techniques allow dynamically typed JavaScript to achieve performance comparable to statically compiled languages.

## JIT Compilation: The Ignition and TurboFan Pipeline

V8 employs a two-tier compilation strategy that balances startup latency with peak execution speed.

### Bytecode Generation with Ignition

The **Ignition** interpreter tokenizes source code into an Abstract Syntax Tree (AST), then generates compact bytecode that executes immediately. This eliminates the long warm-up times associated with ahead-of-time compilation. While interpreting, Ignition records profiling data including argument types, branch outcomes, and property shapes.

### Optimization with TurboFan

When a function becomes *hot*—executed frequently enough to justify optimization—the engine hands the bytecode and profiling data to **TurboFan**, the optimizing compiler. According to `docs/concepts/javascript-engines.mdx`, TurboFan emits native machine code specialized for the observed types and hidden classes. This specialized code assumes specific object layouts and type combinations.

### Deoptimization Guards

If subsequent execution violates TurboFan’s assumptions—such as passing a string where a number was expected—the engine triggers **deoptimization**. It discards the optimized machine code and falls back to Ignition’s bytecode until new profiling data warrants another optimization attempt. This deoptimization mechanism is demonstrated in the test suite at [`tests/fundamentals/javascript-engines/javascript-engines.test.js`](https://github.com/leonardomso/33-js-concepts/blob/main/tests/fundamentals/javascript-engines/javascript-engines.test.js).

## Hidden Classes: Stabilizing Object Shapes

JavaScript objects are dynamic, yet V8 internally assigns **hidden classes** (also called maps or shapes) that define property order and memory offsets.

### Creating Consistent Shapes

When objects are created with identical property lists in the same order, they share the same hidden class. This allows the engine to compute fixed memory offsets for each property. For example, if `obj.x` consistently resides at offset 0, the engine can access it directly without dictionary lookup.

### Transition Chains and Property Order

Adding or deleting properties creates a **transition chain** to a new hidden class. As shown in lines 57-67 of [`tests/fundamentals/javascript-engines/javascript-engines.test.js`](https://github.com/leonardomso/33-js-concepts/blob/main/tests/fundamentals/javascript-engines/javascript-engines.test.js), these transitions link hidden classes together to maintain fast access during object mutation.

Property order critically impacts optimization. Objects created as `{x:1, y:2}` and `{y:2, x:1}` receive different hidden classes, preventing them from sharing optimized code paths. The "Object Shape Consistency" test cases in the repository demonstrate this constraint.

## Inline Caching: Accelerating Property Access

**Inline caching (IC)** leverages hidden classes to eliminate expensive property lookups. At each call site where a property is accessed, V8 caches the memory offset associated with the current hidden class.

### Monomorphic, Polymorphic, and Megamorphic States

The repository's test file [`tests/fundamentals/javascript-engines/javascript-engines.test.js`](https://github.com/leonardomso/33-js-concepts/blob/main/tests/fundamentals/javascript-engines/javascript-engines.test.js) demonstrates three IC states:

- **Monomorphic IC** (lines 33-44): The call site always sees the same hidden class. V8 caches a single offset and uses it for every access, achieving O(1) performance.

- **Polymorphic IC** (lines 45-56): A small number of different hidden classes appear at the call site. V8 stores a limited list of offsets (typically 2-4) and checks them sequentially.

- **Megamorphic IC** (lines 68-84): Many different shapes are observed. The cache abandons optimization and falls back to full dictionary lookup, degrading to O(n) performance.

### Performance Implications

When the inline cache hits, V8 skips the hash table lookup and reads the value directly from the known memory offset. This optimization transforms property access from a potentially expensive dictionary search into a single memory read operation.

## Writing Optimization-Friendly JavaScript

To maximize engine performance, write code that maintains stable object shapes and monomorphic call sites.

### Consistent Object Construction

```javascript
function createPoint(x, y) {
  // Initializing both properties immediately creates one hidden class
  return { x, y };
}

const p1 = createPoint(1, 2);
const p2 = createPoint(3, 4);
// Both objects share the same hidden class, enabling fast inline-cached reads
console.log(p1.x, p2.x);

```

### Avoiding Megamorphic Access Patterns

```javascript
function getX(obj) { return obj.x; }

// Monomorphic: identical shapes keep the IC fast
console.log(getX({ x: 1, y: 2 }));
console.log(getX({ x: 3, y: 4 }));

// Megamorphic: varying property sets force slow dictionary lookup
console.log(getX({ x: 5, a: 1 }));
console.log(getX({ x: 6, b: 2 }));
console.log(getX({ x: 7, c: 3 }));

```

### Managing JIT Warm-Up and Type Stability

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

// Warm-up: numeric calls trigger TurboFan specialization
for (let i = 0; i < 1000; i++) add(i, i + 1);

// Type change violates optimization assumptions, causing deoptimization
console.log(add('hello', 'world'));

```

## Summary

- **JIT compilation** combines Ignition (fast startup via bytecode) with TurboFan (peak performance via machine code), using deoptimization to handle type mismatches safely.
- **Hidden classes** stabilize object memory layouts when properties are added in consistent order, enabling fixed-offset property access.
- **Inline caching** accelerates property reads from O(n) dictionary lookups to O(1) direct access for monomorphic call sites.
- **Optimization-friendly code** maintains consistent object shapes and stable argument types to prevent megamorphic cache states and costly deoptimizations.

## Frequently Asked Questions

### What is the difference between Ignition and TurboFan in V8?

**Ignition** is the bytecode interpreter that executes JavaScript immediately while collecting runtime profiling data. **TurboFan** is the optimizing compiler that generates native machine code for hot functions based on that profiling data. If the optimized code encounters unexpected types, execution falls back to Ignition until recompilation is warranted.

### How do hidden classes improve property access performance?

Hidden classes store the memory offset for each property based on its creation order. When objects share the same hidden class, the engine knows exactly where to find `obj.x` without scanning the entire object or performing hash table lookups. This transforms property access from a dynamic dictionary search into a direct memory read.

### What causes inline caching to become megamorphic?

Inline caching becomes megamorphic when a property access site encounters many different object shapes or hidden classes. Once the cache exceeds its capacity for storing alternative offsets (typically after 4+ distinct shapes), V8 abandons the cache entirely and performs full property lookup, significantly degrading performance.

### How can I avoid deoptimization in performance-critical code?

Maintain consistent object shapes by initializing all properties in the same order during construction and avoiding post-creation deletion. Keep function call sites monomorphic by ensuring arguments maintain consistent types across invocations. These practices allow TurboFan to generate stable optimized machine code without triggering deoptimization guards.