Frontend Analysis Passes
Before the React Compiler generates optimized JavaScript, it performs a series of "frontend" analysis passes. These passes transform your source code into a High-level Intermediate Representation (HIR) and analyze the flow of data to determine what can be safely memoized and what should be pruned for performance.
Lowering to HIR
The first step in the compiler pipeline is Lowering. The compiler converts standard JavaScript and React patterns into an internal Intermediate Representation (HIR).
Lowering simplifies complex syntax (like nested destructuring, various loop types, and conditional expressions) into a flat, instruction-based format. This allows subsequent analysis passes to reason about your code's behavior without worrying about the specific syntactic sugar used to write it.
Validation Passes
The compiler includes several validation passes to ensure your code follows the "Rules of React" and best practices. If a validation fails, the compiler may skip optimization for that specific component to avoid introducing runtime bugs.
Capitalized Call Validation
In React, capitalized functions are reserved for components and should be invoked using JSX (e.g., <MyComponent />), not via direct function calls (e.g., MyComponent()).
The compiler identifies and flags capitalized function calls because calling a component directly bypasses React's lifecycle management and hook resolution.
// ❌ Potential Compiler Error/Warning
function Parent() {
const data = ChildComponent(); // Invalid: components must be rendered via JSX
return <div>{data}</div>;
}
If you have a utility function that is capitalized but is not a component, you can configure the compiler's allowlist to ignore it.
Constant Propagation and Dead Code Elimination
To keep the resulting bundle lean, the compiler performs standard optimization passes:
- Constant Propagation: If a variable has a known constant value at build time, the compiler replaces references to that variable with the constant value itself.
- Dead Code Elimination (DCE): The compiler identifies branches of code that can never be reached (e.g., code after a
returnstatement or inside afalsecondition) and removes them from the IR before memoization scopes are calculated.
Escape Analysis and Scope Pruning
One of the most critical stages is Escape Analysis. The compiler determines which values "escape" the local scope of a component or hook. A value is considered to escape if:
- It is returned by the component.
- It is passed as an argument to a hook (since hooks may store references to their inputs).
- It is used as a dependency in another value that eventually escapes.
Pruning Non-Escaping Scopes
The compiler uses this analysis to "prune" (remove) unnecessary memoization. If a value is created but never escapes and doesn't affect the identity of anything that does escape, the compiler will not generate memoization code for it. This reduces code size and runtime overhead.
function Component(props) {
// This object doesn't escape, isn't used in a hook,
// and isn't returned. The compiler will prune its memoization scope.
const internalConfig = { debug: props.debug };
// This value escapes and will be memoized.
const result = calculateResult(props.input);
return <div>{result}</div>;
}
Interleaved Dependencies
The compiler is also smart enough to detect interleaved mutations. If a non-escaping value is used to mutate an escaping value, the non-escaping value becomes a part of that escaping value's memoization scope to ensure data consistency.
function Component(props) {
const a = [props.a]; // Non-escaping on its own
const b = []; // Escapes via return
const c = {};
c.a = a; // 'a' now affects 'c'
b.push(c); // 'c' now affects 'b'
return b; // 'a' must be memoized to preserve 'b's identity
}
In the example above, even though a isn't returned directly, the compiler recognizes that failing to memoize a would break the memoization of b because they are transitively linked.