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

Output Guarantees

Algorithm

Phase 1: Build the Dependency Graph

Using CollectDependenciesVisitor, build:

Phase 2: Classify Memoization Levels

Each instruction value is classified:

Phase 3: Compute Memoized Identifiers

computeMemoizedIdentifiers() performs a graph traversal starting from escaping values:

Phase 4: Prune Scopes

PruneScopesTransform visits each scope block:

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:

function Component(props) {
  const a = [props.a];

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

  return b;
}

Output:

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: