Reactive Scope Alignment
Overview of Reactive Scopes
The React Compiler transforms components into a High-level Intermediate Representation (HIR) where logic is organized into Reactive Scopes. A reactive scope is a block of instructions that are memoized together. Alignment refers to the process of ensuring these scopes correctly bound computation while minimizing unnecessary overhead.
The compiler performs a critical optimization pass called PruneNonEscapingScopes to remove scopes that do not contribute to values "escaping" the component's local execution context.
Scope Pruning Logic
To minimize code size and runtime overhead, the compiler identifies identifiers that are not required for downstream memoization. A value is considered to "escape" if it meets any of the following criteria:
- Direct Return: The value is explicitly returned by the function.
- Transitive Aliasing: The value is stored inside an object or array that is eventually returned.
- Hook Arguments: The value is passed as an argument to a hook (e.g.,
useEffect,useMemo). Because hooks may store references externally (aliasing), any value passed to them is treated as escaping.
Pruning Algorithm
The alignment and pruning process follows a four-step graph-based approach:
- Graph Construction: A mapping is built between
IdentifierIdand a node describing the instructions and inputs involved. Nodes are categorized:- Aliased: Arrays, objects, and function calls (produce new values).
- Conditionally Aliased: Logic and conditional expressions (depends on the result value).
- Unaliased: JSX elements (though their props may escape).
- Seed Identification: The compiler identifies the initial set of "root" escaping identifiers (return values and hook inputs).
- Traversal: The graph is traversed from the roots. Any dependency reachable through an "aliased" path is marked as escaping.
- Pruning:
ReactiveScopeblocks whose outputs are not marked as escaping are removed.
Transitive Dependency Alignment
A scope must sometimes be preserved even if its primary output does not escape. This occurs when a non-escaping value is a dependency for another scope that does escape.
If the compiler fails to memoize a dependency, the downstream memoized scope will invalidate on every render, breaking the performance guarantee.
function Component(props) {
// 'a' does not escape directly, but it is a dependency for 'c'.
// If 'a' is not memoized, the scope for 'b' and 'c' will
// re-run every render because 'a' is a dependency.
const a = [props.a];
const b = [];
const c = {};
c.a = a; // 'a' is now tied to the scope of 'c'
b.push(props.b);
return b; // 'b' escapes, forcing 'a' to be aligned/memoized.
}
Primitive Dependencies
If a dependency is a primitive type (string, number, boolean), it does not need to be memoized within a scope, even if it is a dependency of an escaping scope. Primitives are compared by value, so they do not trigger invalidation unless their value actually changes.
Validation and Capitalized Calls
Reactive scope alignment requires strict adherence to React's rules for components and hooks. The compiler enforces ValidateNoCapitalizedCalls to ensure that components are not mistakenly treated as standard functions during the alignment phase.
- Rule: Functions starting with a capital letter (likely components) must be invoked via JSX, not called directly as functions.
- Exception: The compiler allows capitalized calls if they are part of a configured allowlist (e.g.,
Boolean,String,Number) or match a specifichookPattern.
// Valid: Hook call or Global
const [state, setState] = React.useState(0);
// Error: Capitalized function call
// This would break scope alignment if the compiler treated it as a normal function.
const x = SomeComponent();
Compilation Modes and Guarantees
The compiler supports different levels of alignment strictness:
- Infer: The compiler automatically decides which scopes to prune based on escape analysis.
- Preserve Existing Memoization: When
@enablePreserveExistingMemoizationGuaranteesis enabled, the compiler ensures that manualuseMemooruseCallbackboundaries are respected and aligned with the generated reactive scopes, even if the values would otherwise be pruned.
Pruning useMemo
If a useMemo block produces a non-escaping value, the compiler may prune the entire manual memoization block to reduce overhead, provided it does not violate the existing memoization guarantees of the environment.