main
md 224 lines 5.77 KB
Rendered Raw
1 # outlineJSX
2
3 ## File
4 `src/Optimization/OutlineJsx.ts`
5
6 ## Purpose
7 This pass outlines nested JSX elements into separate component functions. When a callback function contains JSX, this pass can extract that JSX into a new component, which enables:
8
9 1. **Better code splitting** - Outlined components can be lazily loaded
10 2. **Memoization at component boundaries** - React's reconciliation can skip unchanged subtrees
11 3. **Reduced closure captures** - Outlined components receive props explicitly
12
13 The pass specifically targets JSX within callbacks (like `.map()` callbacks) rather than top-level component returns.
14
15 ## Input Invariants
16 - The `enableJsxOutlining` feature flag must be enabled
17 - The function must be a React component or hook
18 - JSX must appear within a nested function expression (callback)
19
20 ## Output Guarantees
21 - Nested functions containing only JSX returns are extracted as separate components
22 - The original callback is replaced with a call to the outlined component
23 - Captured variables become explicit props to the outlined component
24 - The outlined component is registered with the environment for emission
25
26 ## Algorithm
27
28 ### Phase 1: Identify Outlinable JSX
29 ```typescript
30 function outlineJsxImpl(fn: HIRFunction, outlinedFns: Array<HIRFunction>): void {
31 for (const [, block] of fn.body.blocks) {
32 for (const instr of block.instructions) {
33 if (instr.value.kind === 'FunctionExpression') {
34 const innerFn = instr.value.loweredFunc.func;
35
36 // Check if function only returns JSX
37 if (canOutline(innerFn)) {
38 const outlined = createOutlinedComponent(innerFn);
39 outlinedFns.push(outlined);
40 replaceWithComponentCall(instr, outlined);
41 }
42 }
43 }
44 }
45 }
46 ```
47
48 ### Phase 2: Check Outlinability
49 ```typescript
50 function canOutline(fn: HIRFunction): boolean {
51 // Must have exactly one block with only JSX-related instructions
52 // Must end with returning JSX
53 // Must not have complex control flow
54
55 return (
56 fn.body.blocks.size === 1 &&
57 returnsJSX(fn) &&
58 !hasComplexControlFlow(fn)
59 );
60 }
61 ```
62
63 ### Phase 3: Create Outlined Component
64 ```typescript
65 function createOutlinedComponent(fn: HIRFunction): HIRFunction {
66 // Convert captured context to props
67 const props = fn.context.map(capture => ({
68 name: capture.identifier.name,
69 type: capture.identifier.type,
70 }));
71
72 // Create new component function
73 return {
74 ...fn,
75 params: [{kind: 'Identifier', name: 'props', ...}],
76 context: [], // No captures - all via props
77 };
78 }
79 ```
80
81 ### Phase 4: Replace Original Callback
82 ```typescript
83 function replaceWithComponentCall(instr: Instruction, outlined: HIRFunction): void {
84 // Original: items.map(item => <Stringify item={item} />)
85 // Becomes: items.map(item => <OutlinedComponent item={item} />)
86
87 instr.value = {
88 kind: 'JSX',
89 tag: {kind: 'LoadGlobal', name: outlined.id},
90 props: capturedVariablesToProps(instr.value.context),
91 };
92 }
93 ```
94
95 ### Phase 5: Register Outlined Functions
96 ```typescript
97 export function outlineJSX(fn: HIRFunction): void {
98 const outlinedFns: Array<HIRFunction> = [];
99 outlineJsxImpl(fn, outlinedFns);
100
101 for (const outlinedFn of outlinedFns) {
102 fn.env.outlineFunction(outlinedFn, 'Component');
103 }
104 }
105 ```
106
107 ## Edge Cases
108
109 ### Context Captures
110 Variables captured by the callback become props:
111 ```javascript
112 // Before:
113 items.map(item => <Card key={item.id} user={currentUser} item={item} />)
114
115 // After (outlined):
116 function OutlinedCard(props) {
117 return <Card key={props.item.id} user={props.currentUser} item={props.item} />;
118 }
119 items.map(item => <OutlinedCard currentUser={currentUser} item={item} />)
120 ```
121
122 ### Complex Control Flow
123 Callbacks with conditionals or loops are not outlined:
124 ```javascript
125 // Not outlined - has conditional
126 items.map(item => item.show ? <Card item={item} /> : null)
127 ```
128
129 ### Multiple JSX Returns
130 Only single-JSX-return callbacks are outlined:
131 ```javascript
132 // Not outlined - multiple potential returns
133 items.map(item => {
134 if (item.type === 'a') return <TypeA item={item} />;
135 return <TypeB item={item} />;
136 })
137 ```
138
139 ### Top-Level JSX
140 Only JSX in nested callbacks is outlined, not component return values:
141 ```javascript
142 function Component() {
143 return <div />; // Not outlined - this is the component's return
144 }
145 ```
146
147 ### Recursive Outlining
148 The pass recursively processes outlined components to outline their nested JSX.
149
150 ## TODOs
151 None in the source file.
152
153 ## Example
154
155 ### Fixture: `outlined-helper.js`
156
157 **Input:**
158 ```javascript
159 // @enableFunctionOutlining
160 function Component(props) {
161 return (
162 <div>
163 {props.items.map(item => (
164 <Stringify key={item.id} item={item.name} />
165 ))}
166 </div>
167 );
168 }
169 ```
170
171 **After OutlineJSX:**
172 ```
173 // Outlined component:
174 function _outlined_Component$1(props) {
175 return <Stringify key={props.item.id} item={props.item.name} />;
176 }
177
178 // Original component modified:
179 function Component(props) {
180 return (
181 <div>
182 {props.items.map(item => (
183 <_outlined_Component$1 item={item} />
184 ))}
185 </div>
186 );
187 }
188 ```
189
190 **Generated Code:**
191 ```javascript
192 function _outlined_Component$1(props) {
193 const $ = _c(2);
194 const item = props.item;
195 let t0;
196 if ($[0] !== item.id || $[1] !== item.name) {
197 t0 = <Stringify key={item.id} item={item.name} />;
198 $[0] = item.id;
199 $[1] = item.name;
200 } else {
201 t0 = $[1];
202 }
203 return t0;
204 }
205
206 function Component(props) {
207 const $ = _c(2);
208 let t0;
209 if ($[0] !== props.items) {
210 t0 = props.items.map((item) => <_outlined_Component$1 item={item} />);
211 $[0] = props.items;
212 $[1] = t0;
213 } else {
214 t0 = $[1];
215 }
216 return <div>{t0}</div>;
217 }
218 ```
219
220 Key observations:
221 - The map callback JSX is extracted into `_outlined_Component$1`
222 - The `item` variable becomes a prop instead of a closure capture
223 - The outlined component gets its own memoization cache
224 - This enables React to skip re-rendering unchanged list items