Escape Analysis
To optimize performance and minimize bundle size, the React Compiler performs Escape Analysis. This process determines which values need to be preserved (memoized) across renders and which can be safely treated as standard JavaScript variables.
By identifying "escaping" values, the compiler can prune unnecessary memoization scopes, ensuring that only the data required for downstream reactivity is cached.
What is an "Escaping" Value?
A value is considered to "escape" a component or hook if its lifecycle must be managed by React beyond the immediate execution of the current function. There are two primary ways a value escapes:
- Direct or Transitive Returns: The value is returned by the component or is included in an object/array that is returned.
- Hook Arguments: The value is passed as an argument to a hook (e.g.,
useEffect,useMemo, or a custom hook). React assumes that any value passed to a hook might be stored or used by React's internal state.
Example: Basic Escape Logic
In this example, only the values that contribute to the final output are memoized.
function Component(props) {
// 'a' does not escape: it is not returned or passed to a hook.
// The compiler will not generate memoization for this object.
const a = { id: props.id };
// 'b' escapes: it is wrapped in 'c', which is returned.
// Both 'b' and 'c' will be memoized.
const b = { data: props.data };
const c = [b];
return c;
}
Memoization Pruning
The compiler uses escape analysis to "prune" (remove) reactive scopes that are not necessary to bound downstream computation. This reduces the overhead of checking dependency arrays and managing cache slots in the useMemoCache.
Scopes and Dependencies
If a value is used as a dependency for a scope that does escape, that dependency must also be memoized—even if it doesn't escape on its own. Failing to memoize a dependency would cause the escaping scope to invalidate on every render, breaking the memoization chain.
function Component(props) {
// 'a' does not escape the function, but it is a dependency of 'b'.
// Because 'b' escapes (is returned), 'a' must be memoized to
// ensure 'b' maintains its identity across renders.
const a = [props.a];
const b = useMemo(() => {
return { data: a };
}, [a]);
return b;
}
Special Cases
JSX Elements
JSX elements are generally treated as "unaliased." While the props and children within a JSX element may escape, the compiler evaluates JSX memoization independently to balance code size with runtime performance. Static memoization of JSX allows for precise prop tracking without the overhead of dynamic iteration.
Capitalized Function Calls
The compiler enforces strict rules regarding capitalized function calls during escape analysis. In React, capitalized names are reserved for components.
- Rule: Capitalized functions must be invoked via JSX (
<MyComponent />), not via standard function calls (MyComponent()). - Reasoning: Calling a component as a function bypasses React's hook engine and lifecycle management. The compiler flags these instances because it cannot safely track the escape state of values inside a component-called-as-a-function.
// ❌ Error: Capitalized functions are reserved for components
function Component() {
const x = SomeComponent();
return x;
}
// ✅ Correct: Render components using JSX
function Component() {
return <SomeComponent />;
}
Primitive Values
The compiler typically prunes memoization for primitive values (strings, numbers, booleans) that do not escape to hooks, as the cost of re-calculating a primitive is often lower than the cost of memoization overhead. However, if a primitive is a dependency for a memoized object or array, the compiler ensures the logic leading to that primitive is correctly tracked within the reactive scope.