main
md 138 lines 5.12 KB
Rendered Raw
1 # pruneNonReactiveDependencies
2
3 ## File
4 `src/ReactiveScopes/PruneNonReactiveDependencies.ts`
5
6 ## Purpose
7 This pass removes dependencies from reactive scopes that are guaranteed to be **non-reactive** (i.e., their values cannot change between renders). This optimization reduces unnecessary memoization invalidations by ensuring scopes only depend on values that can actually change.
8
9 The pass complements `PropagateScopeDependencies`, which infers dependencies without considering reactivity. This subsequent pruning step filters out dependencies that are semantically constant.
10
11 ## Input Invariants
12 - The function has been converted to a ReactiveFunction structure
13 - `InferReactivePlaces` has annotated places with `{reactive: true}` where values can change
14 - Each `ReactiveScopeBlock` has a `scope.dependencies` set populated by `PropagateScopeDependenciesHIR`
15 - Type inference has run, so identifiers have type information for `isStableType` checks
16
17 ## Output Guarantees
18 - **Non-reactive dependencies removed**: All dependencies in `scope.dependencies` are reactive after this pass
19 - **Scope outputs marked reactive if needed**: If a scope has any reactive dependencies remaining, all its outputs are marked reactive
20 - **Stable types remain non-reactive through property loads**: When loading properties from stable types (like `useReducer` dispatch functions), the result is not added to the reactive set
21
22 ## Algorithm
23
24 ### Phase 1: Collect Reactive Identifiers
25 The `collectReactiveIdentifiers` helper builds the initial set of reactive identifiers by:
26 1. Visiting all places in the ReactiveFunction
27 2. Adding any place marked `{reactive: true}` to the set
28 3. For pruned scopes, adding declarations that are not primitives and not stable ref types
29
30 ### Phase 2: Propagate Reactivity and Prune Dependencies
31 The main `Visitor` class traverses the ReactiveFunction and:
32
33 1. **For Instructions** - Propagates reactivity through data flow:
34 - `LoadLocal`: If source is reactive, mark the lvalue as reactive
35 - `StoreLocal`: If source value is reactive, mark both the local variable and lvalue as reactive
36 - `Destructure`: If source is reactive, mark all pattern operands as reactive (except stable types)
37 - `PropertyLoad`: If object is reactive AND result is not a stable type, mark result as reactive
38 - `ComputedLoad`: If object OR property is reactive, mark result as reactive
39
40 2. **For Scopes** - Prunes non-reactive dependencies and propagates outputs:
41 - Delete each dependency from `scope.dependencies` if its identifier is not in the reactive set
42 - If any dependencies remain after pruning, mark all scope outputs as reactive
43
44 ### Key Insight: Stable Types
45 The pass leverages `isStableType` to prevent reactivity from flowing through certain React-provided stable values:
46
47 ```typescript
48 function isStableType(id: Identifier): boolean {
49 return (
50 isSetStateType(id) || // useState setter
51 isSetActionStateType(id) || // useActionState setter
52 isDispatcherType(id) || // useReducer dispatcher
53 isUseRefType(id) || // useRef result
54 isStartTransitionType(id) ||// useTransition startTransition
55 isSetOptimisticType(id) // useOptimistic setter
56 );
57 }
58 ```
59
60 ## Edge Cases
61
62 ### Unmemoized Values Spanning Hook Calls
63 A value created before a hook call and mutated after cannot be memoized. However, if it's non-reactive, it still should not appear as a dependency of downstream scopes.
64
65 ### Stable Types from Reactive Containers
66 When `useReducer` returns `[state, dispatch]`, `state` is reactive but `dispatch` is stable. The pass correctly handles this.
67
68 ### Pruned Scopes with Reactive Content
69 The `CollectReactiveIdentifiers` pass also examines pruned scopes and adds their non-primitive, non-stable-ref declarations to the reactive set.
70
71 ### Transitive Reactivity Through Scopes
72 When a scope retains at least one reactive dependency, ALL its outputs become reactive.
73
74 ## TODOs
75 None in the source file.
76
77 ## Example
78
79 ### Fixture: `unmemoized-nonreactive-dependency-is-pruned-as-dependency.js`
80
81 **Input:**
82 ```javascript
83 function Component(props) {
84 const x = [];
85 useNoAlias();
86 mutate(x);
87
88 return <div>{x}</div>;
89 }
90 ```
91
92 **Before PruneNonReactiveDependencies:**
93 ```
94 scope @2 dependencies=[x$15_@0:TObject<BuiltInArray>] declarations=[$23_@2]
95 ```
96
97 **After PruneNonReactiveDependencies:**
98 ```
99 scope @2 dependencies=[] declarations=[$23_@2]
100 ```
101
102 The dependency on `x` is removed because `x` is created locally and therefore non-reactive.
103
104 ### Fixture: `useReducer-returned-dispatcher-is-non-reactive.js`
105
106 **Input:**
107 ```javascript
108 function f() {
109 const [state, dispatch] = useReducer();
110
111 const onClick = () => {
112 dispatch();
113 };
114
115 return <div onClick={onClick} />;
116 }
117 ```
118
119 **Generated Code:**
120 ```javascript
121 function f() {
122 const $ = _c(1);
123 const [, dispatch] = useReducer();
124 let t0;
125 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
126 const onClick = () => {
127 dispatch();
128 };
129 t0 = <div onClick={onClick} />;
130 $[0] = t0;
131 } else {
132 t0 = $[0];
133 }
134 return t0;
135 }
136 ```
137
138 The `onClick` function only captures `dispatch`, which is a stable type. Therefore, `onClick` is non-reactive, and the JSX element can be memoized with zero dependencies.