facebook/react
Compiler passes
A pass-by-pass walkthrough of the React Compiler pipeline. Source: compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts.
The pipeline is a single linear sequence run per function. Below the passes are grouped into phases for readability — the actual file just runs them one after another.
Phase 1: lower the Babel AST to HIR
| Pass | File | What it does |
|---|---|---|
findContextIdentifiers |
compiler/packages/babel-plugin-react-compiler/src/HIR/FindContextIdentifiers.ts |
Walks the surrounding scope and records which identifiers are defined outside the function but reachable inside. These become context Places on the resulting HIRFunction. |
lower |
compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts (large) |
Converts a Babel FunctionDeclaration/ArrowFunctionExpression/FunctionExpression to an HIRFunction — basic blocks, instructions, terminals, phi nodes. |
Phase 2: pre-SSA cleanup
| Pass | File | What it does |
|---|---|---|
pruneMaybeThrows |
compiler/packages/babel-plugin-react-compiler/src/Optimization/PruneMaybeThrows.ts |
Removes MaybeThrow terminals that turn out not to throw, simplifying the CFG. |
validateContextVariableLValues |
Validation/ValidateContextVariableLValues.ts |
Catches assignments to let bindings used outside the function. |
validateUseMemo |
Validation/ValidateUseMemo.ts |
Validates the shape of useMemo calls (callback + deps array). |
dropManualMemoization (optional) |
Inference/DropManualMemoization.ts |
If enableDropManualMemoization is on, replaces user-written useMemo/useCallback with their inner expressions so the compiler can re-memoize uniformly. |
inlineImmediatelyInvokedFunctionExpressions |
Inference/InlineImmediatelyInvokedFunctionExpressions.ts |
(() => x)() → x. |
mergeConsecutiveBlocks |
Inference/MergeConsecutiveBlocks.ts |
Combines blocks with single-successor / single-predecessor relationships. |
Phase 3: SSA + early type info
| Pass | File | What it does |
|---|---|---|
enterSSA |
compiler/packages/babel-plugin-react-compiler/src/SSA/EnterSSA.ts |
Renames identifiers so each Identifier has a single defining instruction; introduces Phi nodes at joins. |
eliminateRedundantPhi |
SSA/EliminateRedundantPhi.ts |
Deletes phi nodes whose operands are all the same. |
constantPropagation |
Optimization/ConstantPropagation.ts |
Propagates compile-time constants. |
inferTypes |
TypeInference/InferTypes.ts |
Lightweight type inference (Object, Array, Function, Primitive, etc.). Used by later passes to decide what's safe to memoize. |
Phase 4: validations that need only types
| Pass | What it catches |
|---|---|
validateHooksUsage |
Hooks called conditionally / in loops / outside React functions. |
validateNoCapitalizedCalls |
Foo() where Foo looks like a component (capitalized identifier) — likely a misuse. |
These run only if env.enableValidations is true (the default for components).
Phase 5: aliasing analysis
| Pass | File | What it does |
|---|---|---|
optimizePropsMethodCalls |
Optimization/OptimizePropsMethodCalls.ts |
Recognizes patterns like props.x.toString() and avoids treating them as full mutations. |
analyseFunctions |
Inference/AnalyseFunctions.ts |
Computes per-instruction aliasing effects for nested function expressions (e.g. event handlers). |
inferMutationAliasingEffects |
Inference/InferMutationAliasingEffects.ts |
Populates Instruction.effects with AliasingEffects — Capture, Alias, Mutate, Freeze, Render, etc. |
optimizeForSSR (SSR mode only) |
Optimization/OptimizeForSSR.ts |
SSR-specific simplifications. |
deadCodeElimination |
Optimization/DeadCodeElimination.ts |
Removes instructions whose results are never used. |
pruneMaybeThrows (again) |
as above | Another pass after DCE. |
inferMutationAliasingRanges |
Inference/InferMutationAliasingRanges.ts |
Computes the range over which each mutation could be observed — a key input to memoization correctness. |
Phase 6: deep validations (Rules of React)
This is where most of the user-visible diagnostics come from. They run in this order, each gated on its own config flag:
| Validation | File |
|---|---|
validateLocalsNotReassignedAfterRender |
Validation/ValidateLocalsNotReassignedAfterRender.ts |
validateNoRefAccessInRender |
Validation/ValidateNoRefAccessInRender.ts (~32 KB — many cases) |
validateNoSetStateInRender |
Validation/ValidateNoSetStateInRender.ts |
validateNoDerivedComputationsInEffects (or its experimental version) |
Validation/ValidateNoDerivedComputationsInEffects.ts / _exp.ts |
validateNoSetStateInEffects (lint mode) |
Validation/ValidateNoSetStateInEffects.ts |
validateNoJSXInTryStatement (lint mode) |
Validation/ValidateNoJSXInTryStatement.ts |
validateNoFreezingKnownMutableFunctions |
Validation/ValidateNoFreezingKnownMutableFunctions.ts |
validateExhaustiveDependencies (after reactivity inference) |
Validation/ValidateExhaustiveDependencies.ts |
validateStaticComponents (lint mode) |
Validation/ValidateStaticComponents.ts |
validatePreservedManualMemoization (later phase) |
Validation/ValidatePreservedManualMemoization.ts |
The biggest of these — ValidateNoRefAccessInRender — has dozens of subtle cases for what counts as a ref access during render.
Phase 7: reactivity inference
| Pass | File | What it does |
|---|---|---|
inferReactivePlaces |
Inference/InferReactivePlaces.ts |
Marks each Place as reactive (depends on props/state/context) or not. |
rewriteInstructionKindsBasedOnReassignment |
SSA/RewriteInstructionKindsBasedOnReassignment.ts |
Converts Reassign instructions back to Assign where the SSA dance allows. |
inferReactiveScopeVariables |
ReactiveScopes/InferReactiveScopeVariables.ts |
Computes the reactive scopes — the spans of HIR that will be wrapped in memo cache lookups. |
If enableMemoization is off, no scopes are created and the rest of the pipeline mostly no-ops. This is how lint mode bypasses memoization.
Phase 8: reactive-tree transformations
After buildReactiveFunction, the IR shifts from HIRFunction to ReactiveFunction — a tree shaped more like the input source code, with reactive scopes annotated. The remaining passes operate on this representation:
| Pass | File | What it does |
|---|---|---|
buildReactiveFunction |
ReactiveScopes/BuildReactiveFunction.ts |
The CFG → tree conversion. |
pruneUnusedLabels |
ReactiveScopes/PruneUnusedLabels.ts |
Discards block labels nothing branches to. |
pruneNonEscapingScopes |
ReactiveScopes/PruneNonEscapingScopes.ts |
Drops scopes whose values never escape — there is nothing to memoize. |
pruneAlwaysInvalidatingScopes |
ReactiveScopes/PruneAlwaysInvalidatingScopes.ts |
Drops scopes that would invalidate every render anyway. |
pruneNonReactiveDependencies |
ReactiveScopes/PruneNonReactiveDependencies.ts |
Removes deps that aren't reactive. |
mergeReactiveScopesThatInvalidateTogether |
ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts |
Coalesces sibling scopes with the same dep set. |
flattenReactiveLoopsHIR |
ReactiveScopes/FlattenReactiveLoopsHIR.ts |
Loops can't host scopes; this lifts them. |
flattenScopesWithHooksOrUseHIR |
ReactiveScopes/FlattenScopesWithHooksOrUseHIR.ts |
Hooks and use(...) can't be inside a memoization block; this lifts them. |
propagateEarlyReturns |
ReactiveScopes/PropagateEarlyReturns.ts |
Ensures early-return paths through scopes are sane. |
propagateScopeDependenciesHIR |
HIR/PropagateScopeDependenciesHIR.ts |
Final dep set computation per scope. |
alignReactiveScopesToBlockScopesHIR |
ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts |
Aligns reactive scopes with JS block boundaries so codegen is clean. |
alignMethodCallScopes |
ReactiveScopes/AlignMethodCallScopes.ts |
Special handling for obj.method(...) calls. |
alignObjectMethodScopes |
ReactiveScopes/AlignObjectMethodScopes.ts |
Object literal methods. |
extractScopeDeclarationsFromDestructuring |
ReactiveScopes/ExtractScopeDeclarationsFromDestructuring.ts |
Pulls declarations out of destructuring patterns. |
pruneHoistedContexts |
ReactiveScopes/PruneHoistedContexts.ts |
Cleans up after hoisting transformations. |
pruneUnusedScopes |
ReactiveScopes/PruneUnusedScopes.ts |
Final scope cleanup. |
pruneUnusedLValues |
ReactiveScopes/PruneUnusedLValues.ts |
Drops dead lvalues. |
renameVariables |
ReactiveScopes/RenameVariables.ts |
Resolves SSA name conflicts before codegen. |
promoteUsedTemporaries |
ReactiveScopes/PromoteUsedTemporaries.ts |
Promotes temporaries that need stable names. |
outlineFunctions |
Optimization/OutlineFunctions.ts |
Hoists nested functions into top-level helpers where it improves dedup. |
outlineJSX |
Optimization/OutlineJsx.ts |
Same for JSX subtrees that don't depend on local state. |
nameAnonymousFunctions |
Transform/NameAnonymousFunctions.ts |
Names anonymous arrow functions for better dev tools / stack traces. |
memoizeFbtAndMacroOperandsInSameScope |
ReactiveScopes/MemoizeFbtAndMacroOperandsInSameScope.ts |
Special-cases for the fbt translation macro and a handful of others. |
stabilizeBlockIds |
ReactiveScopes/StabilizeBlockIds.ts |
Renumbers block ids deterministically so codegen output is stable. |
assertScopeInstructionsWithinScopes |
invariant pass | Sanity check. |
assertWellFormedBreakTargets |
invariant pass | Sanity check. |
Phase 9: codegen
| Pass | File | What it does |
|---|---|---|
codegenFunction |
ReactiveScopes/CodegenReactiveFunction.ts |
Walks the ReactiveFunction and emits Babel AST: const $ = c(N);, the per-scope if ($[i] !== dep) ... blocks, and the original logic interleaved. |
The output is a CodegenFunction ASTish object. The Babel plugin then replaces the original function in the source AST with this output.
Per-pass invariants
Several passes assert structural invariants:
assertConsistentIdentifiers— everyIdentifieris reached only once (post-SSA).assertTerminalSuccessorsExist— block terminals point at real blocks.assertTerminalPredsExist— every block claims the right predecessors.assertValidBlockNesting— block parents form a valid tree.assertValidMutableRanges— mutable ranges respect the SSA dominance frontier.
These are turned on in dev / test builds and silently become no-ops in production builds. They are the first thing to check when a fixture suddenly fails after an unrelated pass change.
Where to read source
For the canonical ordering, see compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts. For each individual pass, the file paths above are the source of truth — most are between 100 and 1000 lines and largely self-contained.
For testing a new pass, see compiler/CLAUDE.md's description of the snap test runner, the error.todo-*.js / error.bug-*.js fixture conventions, and the "compile arbitrary file" workflow (yarn snap compile <path>).
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.