main
md 4.65 KB

pruneNonEscapingScopes

File

src/ReactiveScopes/PruneNonEscapingScopes.ts

Purpose

This 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:

  1. Returned from the function - The value is directly returned or transitively aliased by a return value
  2. 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)

The key insight is that values which never escape the component boundary can be safely recreated on each render without affecting the behavior of consumers.

Input Invariants

  • The input is a ReactiveFunction after scope blocks have been identified
  • Reactive scopes have been assigned to instructions
  • The pass runs after BuildReactiveFunction and PruneUnusedLabels, before PruneNonReactiveDependencies

Output Guarantees

  • Scopes with non-escaping outputs are removed - Their instructions are inlined back into the parent scope/function body
  • Scopes with escaping outputs are retained - Values that escape via return or hook arguments remain memoized
  • 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 invalidation
  • FinishMemoize instructions are marked pruned=true - When a scope is pruned, the associated memoization instructions are flagged

Algorithm

Phase 1: Build the Dependency Graph

Using CollectDependenciesVisitor, build: - Identifier nodes - Each node tracks memoization level, dependencies, scopes, and whether ultimately memoized - Scope nodes - Each scope tracks its dependencies - Escaping values - Identifiers that escape via return or hook arguments

Phase 2: Classify Memoization Levels

Each instruction value is classified: - Memoized: Arrays, objects, function calls, new expressions - always potentially aliasing - Conditional: Conditional/logical expressions, property loads - memoized only if dependencies are memoized - Unmemoized: JSX elements (when memoizeJsxElements is false), DeclareLocal - Never: Primitives, LoadGlobal, binary/unary expressions - can be cheaply compared

Phase 3: Compute Memoized Identifiers

computeMemoizedIdentifiers() performs a graph traversal starting from escaping values: - For each escaping value, recursively visit its dependencies - Mark values and their scopes based on memoization level - When marking a scope, force-memoize all its dependencies

Phase 4: Prune Scopes

PruneScopesTransform visits each scope block: - If any scope output is in the memoized set, keep the scope - If no outputs are memoized, replace the scope block with its inlined instructions

Edge Cases

Interleaved Mutations

const a = [props.a];  // independently memoizable, non-escaping
const b = [];
const c = {};
c.a = a;              // c captures a, but c doesn't escape
b.push(props.b);      // b escapes via return
return b;

Here 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.

Hook Arguments Escape

Values passed to hooks are treated as escaping because hooks may store references internally.

JSX Special Handling

JSX elements are marked as Unmemoized by default because React.memo() can handle dynamic memoization.

noAlias Functions

If a function signature indicates noAlias === true, its arguments are not treated as escaping.

Reassignments

When a scope reassigns a variable, the scope is added as a dependency of that variable.

TODOs

None explicitly in the source file.

Example

Fixture: escape-analysis-non-escaping-interleaved-allocating-dependency.js

Input: ```javascript function Component(props) { const a = [props.a];

const b = []; const c = {}; c.a = a; b.push(props.b);

return b; } ```

Output: ```javascript function Component(props) { const $ = _c(5); let t0; if ($[0] !== props.a) { t0 = [props.a]; $[0] = props.a; $[1] = t0; } else { t0 = $[1]; } const a = t0; // a is memoized even though it doesn't escape directly

let b; if ($[2] !== a || $[3] !== props.b) { b = []; const c = {}; // c is NOT memoized - it doesn't escape c.a = a; b.push(props.b); $[2] = a; $[3] = props.b; $[4] = b; } else { b = $[4]; } return b; } ```

Key observations: - a is memoized because it's a dependency of the scope containing b - c is not separately memoized because it doesn't escape - b is memoized because it's returned