compiler/packages/babel-plugin-react-compiler/docs/passes/23-pruneNonEscapingScopes.md MARKDOWN 131 lines View on github.com → Search inside
1# pruneNonEscapingScopes23## File4`src/ReactiveScopes/PruneNonEscapingScopes.ts`56## Purpose7This pass prunes (removes) reactive scopes whose outputs do not "escape" the component and therefore do not need to be memoized. A value "escapes" in two ways:891. **Returned from the function** - The value is directly returned or transitively aliased by a return value102. **Passed to a hook** - Any value passed as an argument to a hook may be stored by React internally (e.g., the closure passed to `useEffect`)1112The key insight is that values which never escape the component boundary can be safely recreated on each render without affecting the behavior of consumers.1314## Input Invariants15- The input is a `ReactiveFunction` after scope blocks have been identified16- Reactive scopes have been assigned to instructions17- The pass runs after `BuildReactiveFunction` and `PruneUnusedLabels`, before `PruneNonReactiveDependencies`1819## Output Guarantees20- **Scopes with non-escaping outputs are removed** - Their instructions are inlined back into the parent scope/function body21- **Scopes with escaping outputs are retained** - Values that escape via return or hook arguments remain memoized22- **Transitive dependencies of escaping scopes are preserved** - If an escaping scope depends on a non-escaping value, that value's scope is also retained to prevent unnecessary invalidation23- **`FinishMemoize` instructions are marked `pruned=true`** - When a scope is pruned, the associated memoization instructions are flagged2425## Algorithm2627### Phase 1: Build the Dependency Graph28Using `CollectDependenciesVisitor`, build:29- **Identifier nodes** - Each node tracks memoization level, dependencies, scopes, and whether ultimately memoized30- **Scope nodes** - Each scope tracks its dependencies31- **Escaping values** - Identifiers that escape via return or hook arguments3233### Phase 2: Classify Memoization Levels34Each instruction value is classified:35- `Memoized`: Arrays, objects, function calls, `new` expressions - always potentially aliasing36- `Conditional`: Conditional/logical expressions, property loads - memoized only if dependencies are memoized37- `Unmemoized`: JSX elements (when `memoizeJsxElements` is false), DeclareLocal38- `Never`: Primitives, LoadGlobal, binary/unary expressions - can be cheaply compared3940### Phase 3: Compute Memoized Identifiers41`computeMemoizedIdentifiers()` performs a graph traversal starting from escaping values:42- For each escaping value, recursively visit its dependencies43- Mark values and their scopes based on memoization level44- When marking a scope, force-memoize all its dependencies4546### Phase 4: Prune Scopes47`PruneScopesTransform` visits each scope block:48- If any scope output is in the memoized set, keep the scope49- If no outputs are memoized, replace the scope block with its inlined instructions5051## Edge Cases5253### Interleaved Mutations54```javascript55const a = [props.a];  // independently memoizable, non-escaping56const b = [];57const c = {};58c.a = a;              // c captures a, but c doesn't escape59b.push(props.b);      // b escapes via return60return b;61```62Here `a` does not directly escape, but it is a dependency of the scope containing `b`. The algorithm correctly identifies that `a`'s scope must be preserved.6364### Hook Arguments Escape65Values passed to hooks are treated as escaping because hooks may store references internally.6667### JSX Special Handling68JSX elements are marked as `Unmemoized` by default because React.memo() can handle dynamic memoization.6970### noAlias Functions71If a function signature indicates `noAlias === true`, its arguments are not treated as escaping.7273### Reassignments74When a scope reassigns a variable, the scope is added as a dependency of that variable.7576## TODOs77None explicitly in the source file.7879## Example8081### Fixture: `escape-analysis-non-escaping-interleaved-allocating-dependency.js`8283**Input:**84```javascript85function Component(props) {86  const a = [props.a];8788  const b = [];89  const c = {};90  c.a = a;91  b.push(props.b);9293  return b;94}95```9697**Output:**98```javascript99function Component(props) {100  const $ = _c(5);101  let t0;102  if ($[0] !== props.a) {103    t0 = [props.a];104    $[0] = props.a;105    $[1] = t0;106  } else {107    t0 = $[1];108  }109  const a = t0;  // a is memoized even though it doesn't escape directly110111  let b;112  if ($[2] !== a || $[3] !== props.b) {113    b = [];114    const c = {};      // c is NOT memoized - it doesn't escape115    c.a = a;116    b.push(props.b);117    $[2] = a;118    $[3] = props.b;119    $[4] = b;120  } else {121    b = $[4];122  }123  return b;124}125```126127Key observations:128- `a` is memoized because it's a dependency of the scope containing `b`129- `c` is not separately memoized because it doesn't escape130- `b` is memoized because it's returned

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.