Escape Analysis & Pruning
Overview
The PruneNonEscapingScopes pass is a core optimization within the React Compiler (HIR). Its primary objective is to minimize the overhead of memoization by removing reactive scopes for values that do not "escape" the local function context. By identifying values that are neither returned nor passed to external side-effect containers (like hooks), the compiler can avoid generating unnecessary useMemo-like cache structures, reducing both code size and runtime JIT pressure.
Escape Analysis Criteria
A value is defined as escaping if it meets any of the following conditions:
- Direct Return: The value is the return value of the component or hook.
- Transitive Aliasing: The value is used to construct another value that eventually escapes (e.g., an object property or an array element).
- Hook Input: The value is passed as an argument to a hook. Because hooks (like
useEffectoruseLayoutEffect) can store references in React internals or external stores, the compiler treats all hook arguments as escaping to ensure referential stability.
Example: Basic Escaping Logic
function Component(props) {
// 'a' is not aliased or returned: Not memoized, scope pruned.
const a = { value: props.val };
// 'b' is aliased by 'c', which is returned: 'b' must be memoized.
const b = { id: 1 };
const c = [b];
return c;
}
The Pruning Algorithm
The compiler executes the pruning logic in four distinct phases:
- Graph Construction: The pass builds a mapping of
IdentifierIdto nodes describing the instructions and inputs involved in creating that identifier.- Aliased: Arrays, objects, and function calls produce new values and are marked as aliased.
- Conditionally Aliased: Logical expressions (e.g.,
&&,||) and conditionals are marked based on whether their result value is eventually aliased. - Unaliased: JSX elements are initially marked as unaliased (though their props/children are analyzed independently).
- Sink Identification: During the same pass, the compiler identifies "sinks"—returned identifiers and identifiers passed to hooks.
- Reachability Traversal: Starting from the sinks, the compiler traverses the graph backward. Any dependency reachable from a sink is promoted to "escaping."
- Scope Pruning: Any
ReactiveScopewhose outputs were not marked as escaping is removed.
Interleaved Mutations and Memoization Guarantees
The compiler must preserve memoization for non-escaping values if they are dependencies of an escaping scope. If the compiler merges multiple values into a single scope because their mutations interleave, all transitive dependencies of that scope must remain memoized to prevent frequent invalidations.
Case Study: Dependency Chains
function Component(props) {
// 'a' does not escape directly.
// However, 'a' is a dependency of 'c'.
// 'c' is in a scope with 'b', and 'b' is returned.
// To prevent 'b' from re-allocating on every render, 'a' must be memoized.
const a = [props.a];
const b = [];
const c = {};
c.link = a; // Interleaving 'c' and 'b'
b.push(props.b);
return b;
}
Validation: Capitalized Calls
To assist the escape analysis and ensure architectural correctness, the compiler validates function calls. In React, capitalized functions are reserved for components and must be invoked via JSX.
The ValidateNoCapitalizedCalls pass prevents the compiler from incorrectly analyzing components called as standard functions, which would bypass the standard lifecycle and escape analysis logic.
- Rule: Capitalized functions (except those in an allowlist or matching a specific hook pattern) cannot be called directly.
- Reasoning: If a component is called as a function (e.g.,
const x = Header()), the compiler cannot safely determine if the internal values "escape" into the React fiber tree correctly.
// Global/Config allowlist handles standard constructors like String, Number, Boolean.
const b = Boolean(true); // OK
// Error: Capitalized functions must be rendered as <SomeFunc />
const x = SomeFunc();
Impact on Compilation Mode
When @compilationMode("infer") is active, the compiler uses the results of escape analysis to decide whether to compile a function at all. If the PruneNonEscapingScopes pass determines that no values escape (e.g., a function that only performs primitive math and logs to the console), the compiler may opt-out of memoization entirely for that block.
// @expectNothingCompiled
function Component(props) {
// result is a primitive and does not escape to a return or hook
const result = makeObject(props.value).value + 1;
console.log(result);
return 'ok';
}
In this scenario, the overhead of creating a reactive scope outweighs the benefits, as the return value ('ok') is a constant and the intermediate result is non-escaping.