Open-Source Wikis

/

React

/

React Compiler

/

Compiler architecture

facebook/react

Compiler architecture

The React Compiler is a single-pass-per-function Babel/SWC plugin built around a custom HIR (high-level intermediate representation) and a long, linear pipeline of analysis and transformation passes. This page explains its shape; passes catalogs the individual passes.

High-level shape

graph TD
  Babel[Babel/SWC visit FunctionDeclaration] -->|NodePath| Plugin[index.ts: BabelPluginReactCompiler]
  Plugin -->|filter: is this a React fn?| Program[Entrypoint/Program.ts]
  Program -->|per function| Pipeline[Entrypoint/Pipeline.ts]
  Pipeline -->|lower AST → HIR| HIR[HIR.ts]
  HIR -->|N transformation + analysis passes| Reactive[ReactiveFunction]
  Reactive -->|codegen| AST[Codegen → t.FunctionDeclaration]
  AST -->|replace original| Babel

Each pass gets the HIR (or, late in the pipeline, the ReactiveFunction representation that is HIR + reactive scopes), mutates it in place or returns a new structure, and the next pass picks up where it left off. The pipeline is not staged — it is a single linear sequence in compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts.

The Environment

compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts is the per-function context object that flows through every pass. It holds:

  • config: EnvironmentConfig — feature flags + per-pass options.
  • The error accumulator (#errors plus recordError, tryRecord, hasErrors, aggregateErrors — see compiler/CLAUDE.md for a deep discussion).
  • The unique IdentifierId allocator.
  • The currently-known set of context identifiers (vars from the enclosing scope).
  • logger — the optional debug-IR logger.

Once an Environment is constructed in Entrypoint/Pipeline.ts:run, the original EnvironmentConfig goes out of scope — every later pass reaches flags through env.config so the source of truth is unambiguous.

HIR

Defined in compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts. The shape:

HIRFunction {
  body: { blocks: Map<BlockId, BasicBlock>, entry: BlockId },
  context: Place[],          // captured outer-scope vars
  params: Place[],
  returns: Place,
  aliasingEffects: AliasingEffect[],
}

BasicBlock {
  id: BlockId,
  kind: 'block' | 'value' | 'sequence' | 'catch' | 'loop',
  phis: Set<Phi>,
  instructions: Instruction[],
  terminal: Terminal,
  preds: Set<BlockId>,
}

Instruction {
  id: InstructionId,
  lvalue: Place,
  value: InstructionValue,    // CallExpression, FunctionExpression, LoadLocal, ...
  effects?: AliasingEffect[],
}

Place { identifier: Identifier, ... }
Identifier { id: IdentifierId, ... }

It's a classic SSA-friendly CFG with phi nodes at block joins. The enterSSA pass is what turns the post-lower HIR into proper SSA form.

AliasingEffects

Side-effects are first-class. Each instruction can carry a list of AliasingEffects describing what the operation does to the data flow:

  • Capture a -> ba is captured (mutably) into b.
  • Alias a -> bb aliases a.
  • ImmutableCapture a -> b — read-only capture.
  • Assign a -> b — direct assignment.
  • Mutate v / MutateTransitive v / MutateConditionally v — mutation.
  • Render pp is read during render (e.g. JSX prop).
  • Freeze pp is frozen.
  • Create p — new value created.
  • CreateFunction — function expression created (with captures: Place[]).
  • Apply — function application with receiver, function, args, result.

Defined in compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts. The aliasing analysis (inferMutationAliasingEffects) populates these effects; later validation passes consume them.

Hook signatures

compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts is the central registry of known hooks and what they do to their arguments. Each hook is described as:

aliasing: {
  receiver: '@receiver',
  params: ['@fn'],          // names for positional params
  rest: null,
  returns: '@returns',
  temporaries: [],
  effects: [
    { kind: 'Freeze', value: '@fn', reason: ValueReason.HookCaptured },
    { kind: 'Assign', from: '@fn', into: '@returns' },
  ],
}

This is how the compiler knows that useState's setter is stable, that useMemo's callback's argument is read during render, that useEffect's callback's args are not, and that useEffectEvent's callback's args are not (which avoids a class of false positives — see compiler/CLAUDE.md's discussion of UseEffectEventHook).

Pipeline shape

Pipeline.ts is ~560 lines. Roughly:

  1. Lowering: lower(func, env) walks the Babel AST and produces an HIR HIRFunction.
  2. Pre-SSA cleanup: pruneMaybeThrows, inlineImmediatelyInvokedFunctionExpressions, mergeConsecutiveBlocks.
  3. SSA: enterSSA, eliminateRedundantPhi.
  4. Constant propagation, type inference: constantPropagation, inferTypes.
  5. Aliasing inference: analyseFunctions, inferMutationAliasingEffects, deadCodeElimination, inferMutationAliasingRanges.
  6. Validation: a long list — validateHooksUsage, validateNoCapitalizedCalls, validateNoRefAccessInRender, validateNoSetStateInRender, validateNoSetStateInEffects, validateNoDerivedComputationsInEffects, validateNoFreezingKnownMutableFunctions, validateLocalsNotReassignedAfterRender, validateNoJSXInTryStatement, validateStaticComponents, validateExhaustiveDependencies, validatePreservedManualMemoization, validateUseMemo, validateContextVariableLValues.
  7. Reactive scope inference: inferReactivePlaces, inferReactiveScopeVariables. This is what determines the units of memoization — the spans of code that will be wrapped in if ($[i] !== x) ... blocks.
  8. Reactive-tree transformations: buildReactiveFunction, then a long list of cleanups: pruneNonEscapingScopes, pruneAlwaysInvalidatingScopes, pruneNonReactiveDependencies, mergeReactiveScopesThatInvalidateTogether, flattenReactiveLoopsHIR, flattenScopesWithHooksOrUseHIR, propagateEarlyReturns, propagateScopeDependenciesHIR, alignReactiveScopesToBlockScopesHIR, alignMethodCallScopes, alignObjectMethodScopes, extractScopeDeclarationsFromDestructuring, pruneHoistedContexts, pruneUnusedScopes, pruneUnusedLValues, pruneUnusedLabels, renameVariables, promoteUsedTemporaries, outlineFunctions, outlineJSX, nameAnonymousFunctions, stabilizeBlockIds.
  9. Codegen: codegenFunction(reactiveFn) returns an AST that the Babel plugin substitutes for the original FunctionDeclaration.

Each step in Pipeline.ts is preceded by an env.logger?.debugLogIRs?.({...}) call that, when yarn snap -d is on, dumps the HIR after that step. This is the primary debugging tool when working on a pass.

Fault tolerance

Validation passes are wrapped in env.tryRecord(() => pass(hir)). If they throw a CompilerError.throwTodo() (graceful bailout) or a non-invariant CompilerError, the error is recorded and pipeline execution continues. CompilerError.invariant() is reserved for "this should be impossible" and is not caught — that aborts the pipeline immediately.

Infrastructure passes (lowering, SSA construction, codegen) are not wrapped because later passes depend on their structural correctness.

At the end of the pipeline, env.hasErrors() is checked and env.aggregateErrors() produces a single CompilerError describing every recorded problem. The Babel plugin entry decides what to do with that — bail out on the function and leave it un-compiled, surface as an ESLint diagnostic, or fail the build, depending on outputMode.

Output modes

CompilerOutputMode (in Entrypoint/index.ts) selects the operating mode:

  • 'ast' — full compilation: emit the rewritten function. The default for the Babel plugin.
  • 'lint' — only run validations; skip codegen. Used by the ESLint plugin and react-forgive.
  • 'ssr' — variant that calls optimizeForSSR(hir) before reactive-scope inference.

Where to make a change

  • A new validation rule: add a new file under compiler/packages/babel-plugin-react-compiler/src/Validation/, then register it in the Validation/index.ts export list and call it conditionally from the validation block of Pipeline.ts. Add a compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.<descriptive-name>.js fixture that exercises it.
  • A new optimization or transformation: add a file under Optimization/ or ReactiveScopes/, plug it into Pipeline.ts at the right spot, and add fixtures.
  • A new hook to teach the compiler about: edit compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts to add the hook's aliasing signature.
  • A new feature flag: add it in EnvironmentConfig (HIR/Environment.ts), then gate the new code on env.config.<flagName>.

For a step-by-step walkthrough of every pass and what it produces, see passes.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Compiler architecture – React wiki | Factory