Skip to main content

How React Compiler Works

React Compiler transforms React components through a multi-phase pipeline, converting your code into an optimized version with automatic memoization.

High-Level Architecture

The compiler pipeline consists of seven major phases:

Phase 1: HIR Construction

Lowering to HIR

The compiler first converts Babel’s Abstract Syntax Tree (AST) to a High-level Intermediate Representation (HIR). HIR represents code as a control-flow graph (CFG) with:
  • Basic blocks: Sequences of instructions
  • Instructions: Individual operations
  • Terminals: Control flow between blocks (if, return, loop, etc.)
// Original code
function foo(x, y) {
  if (x) {
    return foo(false, y);
  }
  return [y * 10];
}
// HIR representation
foo(x$0, y$1): $12
bb0 (block):
  [1] $6 = LoadLocal x$0
  [2] If ($6) then:bb2 else:bb1

bb2 (block):
  [3] $2 = LoadGlobal foo
  [4] $3 = false
  [5] $4 = LoadLocal y$1
  [6] $5 = Call $2($3, $4)
  [7] Return $5

bb1 (block):
  [8] $7 = LoadLocal y$1
  [9] $8 = 10
  [10] $9 = Binary $7 * $8
  [11] $10 = Array [$9]
  [12] Return $10

SSA Conversion

The HIR is converted to Static Single Assignment (SSA) form, where:
  • Each variable is assigned exactly once
  • Phi nodes represent values from different control flow paths
  • Enables precise dataflow analysis
// Original
let x;
if (condition) {
  x = 1;
} else {
  x = 2;
}
return x;
// SSA form with phi node
let x$1;
if (condition) {
  x$2 = 1;
} else {
  x$3 = 2;
}
x$4 = phi(x$2, x$3)  // Merge point
return x$4;

Phase 2: Optimization

Early optimization passes improve code quality:

Constant Propagation

Replaces variables with known constant values:
// Before
const x = 5;
const y = x + 3;

// After
const x = 5;
const y = 8;

Dead Code Elimination

Removes unreferenced instructions:
// Before
const unused = expensiveCalculation();
const used = simpleValue();
return used;

// After
const used = simpleValue();
return used;

Phase 3: Type & Effect Inference

Type Inference

The compiler infers types using constraint-based unification:
  • Primitives (string, number, boolean, null, undefined)
  • Objects (with shape information)
  • Functions (including hooks)
  • Arrays and JSX elements
const [count, setCount] = useState(0);
// Inferred types:
// - useState: Function<BuiltInUseState>
// - count: Primitive
// - setCount: Function<BuiltInSetState>

Effect Inference

Infers aliasing and mutation effects through abstract interpretation: Effect Types:
// Data flow effects
Capture a -> b      // b captures a mutably
Alias a -> b        // b aliases a
Assign a -> b       // Direct assignment
Freeze value        // Make immutable

// Mutation effects
Mutate value        // Direct mutation
MutateTransitive    // Mutates transitively

// Special effects
Render place        // Used in JSX/render
Impure place        // Contains impure value
Example:
function Component({ items }) {
  const filtered = items.filter(x => x.active);
  return <List data={filtered} />;
}
// Effects:
// items: Render (used in JSX context)
// filtered: Capture items -> filtered
// filtered: Render (passed to JSX)

Phase 4: Reactive Scope Construction

Inferring Reactive Scopes

The compiler groups instructions that should invalidate together into reactive scopes:
function Component({ a, b }) {
  // Scope 1: depends on 'a'
  const x = a * 2;
  const y = x + 1;
  
  // Scope 2: depends on 'b'
  const z = b * 3;
  
  return { x, y, z };
}

Scope Alignment

Scopes are aligned to:
  • Control flow boundaries (if/else, loops)
  • Method call receivers
  • Object method declarations
  • Block scopes

Scope Merging

Overlapping or always-invalidating-together scopes are merged:
// Before: two scopes
const x = props.a;
const y = x + 1;  // Scope 1
const z = y + 2;  // Scope 2

// After: merged into one scope
const x = props.a;
const y = x + 1;
const z = y + 2;  // Single scope

Phase 5: HIR → Reactive Function

The control-flow graph is converted to a tree structure (ReactiveFunction) where:
  • Scopes become explicit nodes
  • Dependencies are tracked
  • Nesting relationships are preserved

Phase 6: Reactive Function Optimization

Pruning Passes

Multiple passes prune unnecessary scopes:
  • Non-escaping scopes: Values that don’t escape the function
  • Scopes with hooks: Can’t memoize hook calls
  • Always-invalidating: Scopes that always recompute
  • Unused scopes: Empty or unreferenced scopes

Dependency Optimization

// Before: over-specified dependency
const x = props.user.profile.name;
// Depends on: props

// After: precise dependency
const x = props.user.profile.name;
// Depends on: props.user.profile.name

Phase 7: Code Generation

The final phase generates optimized JavaScript:

Memoization Cache

import { c as _c } from "react/compiler-runtime";

function Component(props) {
  const $ = _c(4);  // Create cache with 4 slots
  
  let t0;
  if ($[0] !== props.value) {  // Check dependency
    t0 = expensiveCalc(props.value);  // Recompute
    $[0] = props.value;  // Update dependency
    $[1] = t0;           // Cache result
  } else {
    t0 = $[1];  // Use cached result
  }
  
  return <div>{t0}</div>;
}

Scope Structure

// Multiple reactive scopes
function Component({ a, b, c }) {
  const $ = _c(6);
  
  // Scope 1: depends on 'a'
  let t0;
  if ($[0] !== a) {
    t0 = computeX(a);
    $[0] = a;
    $[1] = t0;
  } else {
    t0 = $[1];
  }
  
  // Scope 2: depends on 'b'
  let t1;
  if ($[2] !== b) {
    t1 = computeY(b);
    $[2] = b;
    $[3] = t1;
  } else {
    t1 = $[3];
  }
  
  // Scope 3: depends on t0 and t1
  let t2;
  if ($[4] !== t0 || $[5] !== t1) {
    t2 = <div>{t0} {t1}</div>;
    $[4] = t0;
    $[5] = t1;
    $[6] = t2;
  } else {
    t2 = $[6];
  }
  
  return t2;
}

When the Compiler Bails Out

The compiler may bail out (skip compilation) when:

Unsupported JavaScript Features

  • eval() calls
  • with statements
  • var declarations (use let/const)
  • Nested class declarations

Rule Violations

  • Conditional hook calls
  • Unconditional setState during render
  • Direct ref.current access in render
  • Mutations after value is frozen

Complex Patterns

  • Deeply nested closures capturing mutable state
  • Dynamically created components
  • Certain imperative patterns

Compiler Error Types

Todo Errors (Graceful Bailout)

Todo: Support [feature]
Known limitation, compiler skips this function but continues.

Invariant Errors (Hard Failure)

Invariant violation: [condition]
Unexpected state, indicates compiler bug or invalid input.

Validation Errors

InvalidReact: [description]
Code violates Rules of React, must be fixed.

Performance Characteristics

Compilation Time

  • Typically 1-5ms per component
  • Scales linearly with code size
  • Cached between builds

Runtime Overhead

  • Memoization checks: O(1) per scope
  • Cache storage: ~1-2 slots per reactive value
  • Minimal memory overhead

Optimization Benefits

  • Reduces re-renders by 50-90% in typical apps
  • Eliminates need for manual memoization
  • Improves performance on low-end devices

Next Steps

Architecture

Deep dive into compiler passes

HIR

Understanding the intermediate representation

Optimization Passes

Learn about specific optimizations

Configuration

Configure compiler behavior

Build docs developers (and LLMs) love