main
md 187 lines 4.93 KB
Rendered Raw
1 # optimizeForSSR
2
3 ## File
4 `src/Optimization/OptimizeForSSR.ts`
5
6 ## Purpose
7 This pass applies Server-Side Rendering (SSR) specific optimizations. During SSR, React renders components to HTML strings without mounting them in the DOM. This means:
8
9 1. **Effects don't run** - `useEffect` and `useLayoutEffect` are no-ops
10 2. **Event handlers aren't needed** - There's no DOM to attach handlers to
11 3. **State is never updated** - Components render once with initial state
12 4. **Refs aren't attached** - There's no DOM to ref
13
14 The pass leverages these SSR characteristics to inline and simplify code, removing unnecessary runtime overhead.
15
16 ## Input Invariants
17 - The function has been through type inference
18 - Hook types are properly identified (useState, useReducer, useEffect, etc.)
19 - Function types for callbacks are properly inferred
20
21 ## Output Guarantees
22 - `useState(initialValue)` is inlined to just `[initialValue, noop]`
23 - `useReducer(reducer, initialArg, init?)` is inlined to `[init ? init(initialArg) : initialArg, noop]`
24 - `useEffect` and `useLayoutEffect` calls are removed entirely
25 - Event handler functions (functions that call setState) are replaced with empty functions
26 - Ref-typed values are removed from JSX props
27
28 ## Algorithm
29
30 ### Phase 1: Identify Inlinable State
31 ```typescript
32 const inlinedState = new Map<IdentifierId, InstructionValue>();
33
34 for (const instr of block.instructions) {
35 if (isUseStateCall(instr)) {
36 // Store the initial value for inlining
37 inlinedState.set(instr.lvalue.id, {
38 kind: 'ArrayExpression',
39 elements: [initialValue, noopFunction],
40 });
41 }
42
43 if (isUseReducerCall(instr)) {
44 // Compute initial state and store for inlining
45 const initialState = init ? callInit(initialArg) : initialArg;
46 inlinedState.set(instr.lvalue.id, {
47 kind: 'ArrayExpression',
48 elements: [initialState, noopFunction],
49 });
50 }
51 }
52 ```
53
54 ### Phase 2: Inline State Hooks
55 Replace useState/useReducer with their computed initial values:
56 ```typescript
57 // Before:
58 $0 = useState(0)
59 [state, setState] = $0
60
61 // After (inlined):
62 $0 = [0, () => {}]
63 [state, setState] = $0
64 ```
65
66 ### Phase 3: Remove Effects
67 ```typescript
68 if (isUseEffectCall(instr) || isUseLayoutEffectCall(instr)) {
69 // Remove the instruction entirely
70 block.instructions.splice(i, 1);
71 }
72 ```
73
74 ### Phase 4: Identify and Neuter Event Handlers
75 ```typescript
76 // Functions that capture and call setState are event handlers
77 if (capturesSetState(functionExpr)) {
78 // Replace with empty function
79 instr.value = {
80 kind: 'FunctionExpression',
81 params: originalParams,
82 body: emptyBody,
83 };
84 }
85 ```
86
87 ### Phase 5: Remove Ref Props
88 ```typescript
89 if (isJSX(instr) && hasRefProp(instr)) {
90 // Remove ref={...} from JSX props
91 removeRefProp(instr.value);
92 }
93 ```
94
95 ## Edge Cases
96
97 ### useState with Function Initializer
98 When `useState` receives a function initializer, it must be called:
99 ```javascript
100 // useState(() => expensive())
101 // SSR: Call the initializer to get the value
102 const [state] = [expensiveComputation(), noop];
103 ```
104
105 ### useReducer with Init Function
106 The optional `init` function is called with `initialArg`:
107 ```javascript
108 // useReducer(reducer, arg, init)
109 // SSR: [init(arg), noop]
110 ```
111
112 ### Nested State Setters
113 Functions that transitively call setState are also event handlers:
114 ```javascript
115 function outer() {
116 function inner() {
117 setState(x); // inner is event handler
118 }
119 inner(); // outer is also event handler
120 }
121 ```
122
123 ### Conditional Event Handlers
124 Event handler detection is conservative - if a function might call setState, it's treated as an event handler.
125
126 ### Refs in Nested Objects
127 Only direct `ref` props on JSX are removed:
128 ```javascript
129 <div ref={myRef} /> // ref removed
130 <div config={{ref: myRef}} /> // ref NOT removed (nested)
131 ```
132
133 ## TODOs
134 None in the source file.
135
136 ## Example
137
138 ### Fixture: `ssr/optimize-ssr.js`
139
140 **Input:**
141 ```javascript
142 function Component() {
143 const [state, setState] = useState(0);
144 const ref = useRef(null);
145 const onChange = (e) => {
146 setState(e.target.value);
147 };
148 useEffect(() => {
149 log(ref.current.value);
150 });
151 return <input value={state} onChange={onChange} ref={ref} />;
152 }
153 ```
154
155 **After SSR Optimization:**
156 ```javascript
157 function Component() {
158 const $ = _c(1);
159 // useState inlined to [initialValue, noop]
160 const [state] = [0, () => {}];
161
162 // useRef returns object with current: null
163 const ref = { current: null };
164
165 // Event handler replaced with noop (it calls setState)
166 const onChange = () => {};
167
168 // useEffect removed entirely (no-op on SSR)
169
170 // ref prop removed from JSX
171 let t0;
172 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
173 t0 = <input value={state} onChange={onChange} />;
174 $[0] = t0;
175 } else {
176 t0 = $[0];
177 }
178 return t0;
179 }
180 ```
181
182 Key observations:
183 - `useState(0)` becomes `[0, () => {}]` - no hook call
184 - `useEffect(...)` is removed entirely
185 - `onChange` is replaced with empty function since it called `setState`
186 - `ref={ref}` prop is removed from JSX
187 - SSR output is simpler and has less runtime overhead