SSA & Data Flow Analysis
SSA and Data Flow Analysis
The React Compiler utilizes a High-level Intermediate Representation (HIR) transformed into Static Single Assignment (SSA) form to perform sophisticated data flow analysis. This architecture allows the compiler to reason about value lifetimes, mutability, and dependencies with high precision, which is foundational for automatic memoization.
Static Single Assignment (SSA) in HIR
In SSA form, every identifier is assigned exactly once. This transformation simplifies the compiler's ability to track the flow of data across blocks and instructions. When a variable is reassigned in the original source code, the compiler generates a new version of the identifier (e.g., x_0, x_1) and inserts Phi nodes at join points in the control flow graph where multiple versions of a variable might converge.
By converting to SSA, the compiler can:
- Determine precise dependency graphs: Identify exactly which version of a value is consumed by a specific instruction.
- Perform alias analysis: Track how values are moved, copied, or referenced across different scopes.
- Simplify Pruning: Easily identify unused or redundant assignments that do not contribute to the final output.
Escape Analysis and Scope Pruning
A critical phase of the compiler’s data flow analysis is Escape Analysis, implemented in the PruneNonEscapingScopes pass. This pass identifies which values "escape" the local function context. A value is considered to escape if:
- Return Values: It is directly returned by the function or transitively aliased by a value that is returned.
- Hook Inputs: It is passed as an argument to a hook (e.g.,
useEffect,useMemo). Because hooks may store references externally (e.g., React's internal state), the compiler must assume these values escape.
Pruning Logic
The compiler prunes reactive scopes (memoization blocks) for identifiers that do not escape. This reduces code size and runtime overhead by avoiding unnecessary caching of intermediate, internal values.
function Component(props) {
// 'a' does not escape and is not a dependency of an escaping value.
// The compiler will not create a memoization scope for 'a'.
const a = { value: props.a };
// 'b' is returned, so it escapes.
// The compiler will generate a reactive scope for 'b'.
const b = { value: props.b };
return b;
}
Dependency Tracking and Interleaved Mutations
Escape analysis is not purely local. The compiler must account for interleaved mutations where a non-escaping value acts as a dependency for an escaping one. To avoid "breaking" downstream memoization, the compiler follows this rule:
If a scope produces a memoized output, all of that scope's transitive dependencies must also be memoized, even if those dependencies do not escape.
Failing to memoize a dependency would cause its parent scope to invalidate on every render, effectively disabling the memoization of the escaping value.
function Component(props) {
// 'a' does not escape, but it is a dependency for 'c'.
const a = [props.a];
// 'b' escapes (returned).
// 'c' does not escape but is interleaved in the same scope as 'b'.
// 'a' must be memoized because it is a dependency of 'c',
// and 'c' shares a scope with the escaping 'b'.
const b = [];
const c = {};
c.a = a;
b.push(props.b);
return b;
}
Validation Passes
The compiler uses the SSA-based data flow graph to perform various validation checks to ensure React architectural patterns are respected.
Capitalized Call Validation
The ValidateNoCapitalizedCalls pass inspects CallExpression and MethodCall instructions. In React, capitalized identifiers are reserved for components and should be invoked via JSX, not called as standard functions.
The compiler flags an error if:
- A global or local variable starting with a capital letter (and not in an allow-list) is invoked as a function.
- A property load resulting in a capitalized name is used as a method call.
Example of an invalid call identified during analysis:
function Foo() {
const x = Bar; // Loaded as a global or local
x(); // Error: Capitalized functions are reserved for components
}
Technical Summary of the Analysis Pipeline
- Lowering: JavaScript source is lowered into HIR.
- SSA Transformation: Identifiers are versioned, and Phi nodes are inserted.
- Alias & Effect Inference: The compiler determines which values are mutated and which variables point to the same underlying data.
- Graph Building: A mapping of
IdentifierIdto its creation nodes (arrays, objects, calls) is constructed. - Escape Marking: Starting from
Returnterminals andHookcalls, the compiler traverses the graph backwards to mark all reachable dependencies as "escaping." - Scope Pruning: Scopes associated with non-escaping, non-essential identifiers are removed from the reactive IR.