Skip to content

Commit 5fa5200

Browse files
committed
[compiler][poc] Quick experiment with SSR-optimization pass
Just a quick poc: * Inline useState when the initializer is known to not be a function. The heuristic could be improved but will handle a large number of cases already. * Prune effects * Prune useRef if the ref is unused, by pruning 'ref' props on primitive components. Then DCE does the rest of the work - with a small change to allow `useRef()` calls to be dropped since function calls aren't normally eligible for dropping. * Prune event handlers, by pruning props whose names start w "on" from primitive components. Then DCE removes the functions themselves. Per the fixture, this gets pretty far.
1 parent 93fc574 commit 5fa5200

16 files changed

+576
-5
lines changed

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRan
105105
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
106106
import {validateNoDerivedComputationsInEffects_exp} from '../Validation/ValidateNoDerivedComputationsInEffects_exp';
107107
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
108+
import {optimizeForSSR} from '../Optimization/OptimizeForSSR';
108109
import {validateSourceLocations} from '../Validation/ValidateSourceLocations';
109110

110111
export type CompilerPipelineValue =
@@ -237,6 +238,11 @@ function runWithEnvironment(
237238
}
238239
}
239240

241+
if (env.config.enableOptimizeForSSR) {
242+
optimizeForSSR(hir);
243+
log({kind: 'hir', name: 'OptimizeForSSR', value: hir});
244+
}
245+
240246
// Note: Has to come after infer reference effects because "dead" code may still affect inference
241247
deadCodeElimination(hir);
242248
log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
@@ -314,8 +320,10 @@ function runWithEnvironment(
314320
* if inferred memoization is enabled. This makes all later passes which
315321
* transform reactive-scope labeled instructions no-ops.
316322
*/
317-
inferReactiveScopeVariables(hir);
318-
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
323+
if (!env.config.enableOptimizeForSSR) {
324+
inferReactiveScopeVariables(hir);
325+
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
326+
}
319327
}
320328

321329
const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);

compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,8 @@ export const EnvironmentConfigSchema = z.object({
677677
* from refs need to be stored in state during mount.
678678
*/
679679
enableAllowSetStateFromRefsInEffects: z.boolean().default(true),
680+
681+
enableOptimizeForSSR: z.boolean().default(false),
680682
});
681683

682684
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;

compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1823,6 +1823,10 @@ export function isPrimitiveType(id: Identifier): boolean {
18231823
return id.type.kind === 'Primitive';
18241824
}
18251825

1826+
export function isPlainObjectType(id: Identifier): boolean {
1827+
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInObject';
1828+
}
1829+
18261830
export function isArrayType(id: Identifier): boolean {
18271831
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInArray';
18281832
}

compiler/packages/babel-plugin-react-compiler/src/Optimization/DeadCodeElimination.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
import {
99
BlockId,
10+
Environment,
11+
getHookKind,
1012
HIRFunction,
1113
Identifier,
1214
IdentifierId,
@@ -68,9 +70,14 @@ export function deadCodeElimination(fn: HIRFunction): void {
6870
}
6971

7072
class State {
73+
env: Environment;
7174
named: Set<string> = new Set();
7275
identifiers: Set<IdentifierId> = new Set();
7376

77+
constructor(env: Environment) {
78+
this.env = env;
79+
}
80+
7481
// Mark the identifier as being referenced (not dead code)
7582
reference(identifier: Identifier): void {
7683
this.identifiers.add(identifier.id);
@@ -112,7 +119,7 @@ function findReferencedIdentifiers(fn: HIRFunction): State {
112119
const hasLoop = hasBackEdge(fn);
113120
const reversedBlocks = [...fn.body.blocks.values()].reverse();
114121

115-
const state = new State();
122+
const state = new State(fn.env);
116123
let size = state.count;
117124
do {
118125
size = state.count;
@@ -310,12 +317,27 @@ function pruneableValue(value: InstructionValue, state: State): boolean {
310317
// explicitly retain debugger statements to not break debugging workflows
311318
return false;
312319
}
313-
case 'Await':
314320
case 'CallExpression':
321+
case 'MethodCall': {
322+
if (state.env.config.enableOptimizeForSSR) {
323+
const calleee =
324+
value.kind === 'CallExpression' ? value.callee : value.property;
325+
const hookKind = getHookKind(state.env, calleee.identifier);
326+
switch (hookKind) {
327+
case 'useState':
328+
case 'useReducer':
329+
case 'useRef': {
330+
// unused refs can be removed
331+
return true;
332+
}
333+
}
334+
}
335+
return false;
336+
}
337+
case 'Await':
315338
case 'ComputedDelete':
316339
case 'ComputedStore':
317340
case 'PropertyDelete':
318-
case 'MethodCall':
319341
case 'PropertyStore':
320342
case 'StoreGlobal': {
321343
/*
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import {CompilerError} from '..';
9+
import {
10+
CallExpression,
11+
getHookKind,
12+
HIRFunction,
13+
IdentifierId,
14+
InstructionValue,
15+
isArrayType,
16+
isPlainObjectType,
17+
isPrimitiveType,
18+
isSetStateType,
19+
isStartTransitionType,
20+
LoadLocal,
21+
StoreLocal,
22+
} from '../HIR';
23+
import {
24+
eachInstructionValueOperand,
25+
eachTerminalOperand,
26+
} from '../HIR/visitors';
27+
import {retainWhere} from '../Utils/utils';
28+
29+
/**
30+
* Optimizes the code for running specifically in an SSR environment. This optimization
31+
* asssumes that setState will not be called during render during initial mount, which
32+
* allows inlining useState/useReducer.
33+
*
34+
* Optimizations:
35+
* - Inline useState/useReducer
36+
* - Remove effects
37+
* - Remove refs where known to be unused during render (eg directly passed to a dom node)
38+
* - Remove event handlers
39+
*
40+
* Note that an earlier pass already inlines useMemo/useCallback
41+
*/
42+
export function optimizeForSSR(fn: HIRFunction): void {
43+
const inlinedState = new Map<IdentifierId, InstructionValue>();
44+
/**
45+
* First pass identifies useState/useReducer which can be safely inlined. Any use
46+
* of the hook return other than destructuring (with a specific pattern) prevents
47+
* inlining.
48+
*
49+
* Supported cases:
50+
* - `const [state, ] = useState( <primitive-array-or-object> )`
51+
* - `const [state, ] = useReducer(..., <value>)`
52+
* - `const [state, ] = useReducer[..., <value>, <init>]`
53+
*/
54+
for (const block of fn.body.blocks.values()) {
55+
for (const instr of block.instructions) {
56+
const {value} = instr;
57+
switch (value.kind) {
58+
case 'Destructure': {
59+
if (
60+
inlinedState.has(value.value.identifier.id) &&
61+
value.lvalue.pattern.kind === 'ArrayPattern' &&
62+
value.lvalue.pattern.items.length >= 1 &&
63+
value.lvalue.pattern.items[0].kind === 'Identifier'
64+
) {
65+
// Allow destructuring of inlined states
66+
continue;
67+
}
68+
break;
69+
}
70+
case 'MethodCall':
71+
case 'CallExpression': {
72+
const calleee =
73+
value.kind === 'CallExpression' ? value.callee : value.property;
74+
const hookKind = getHookKind(fn.env, calleee.identifier);
75+
switch (hookKind) {
76+
case 'useReducer': {
77+
if (
78+
value.args.length === 2 &&
79+
value.args[1].kind === 'Identifier'
80+
) {
81+
const arg = value.args[1];
82+
const replace: LoadLocal = {
83+
kind: 'LoadLocal',
84+
place: arg,
85+
loc: arg.loc,
86+
};
87+
inlinedState.set(instr.lvalue.identifier.id, replace);
88+
} else if (
89+
value.args.length === 3 &&
90+
value.args[1].kind === 'Identifier' &&
91+
value.args[2].kind === 'Identifier'
92+
) {
93+
const arg = value.args[1];
94+
const initializer = value.args[2];
95+
const replace: CallExpression = {
96+
kind: 'CallExpression',
97+
callee: initializer,
98+
args: [arg],
99+
loc: value.loc,
100+
};
101+
inlinedState.set(instr.lvalue.identifier.id, replace);
102+
}
103+
break;
104+
}
105+
case 'useState': {
106+
if (
107+
value.args.length === 1 &&
108+
value.args[0].kind === 'Identifier'
109+
) {
110+
const arg = value.args[0];
111+
if (
112+
isPrimitiveType(arg.identifier) ||
113+
isPlainObjectType(arg.identifier) ||
114+
isArrayType(arg.identifier)
115+
) {
116+
const replace: LoadLocal = {
117+
kind: 'LoadLocal',
118+
place: arg,
119+
loc: arg.loc,
120+
};
121+
inlinedState.set(instr.lvalue.identifier.id, replace);
122+
}
123+
}
124+
break;
125+
}
126+
}
127+
}
128+
}
129+
// Any use of useState/useReducer return besides destructuring prevents inlining
130+
if (inlinedState.size !== 0) {
131+
for (const operand of eachInstructionValueOperand(value)) {
132+
inlinedState.delete(operand.identifier.id);
133+
}
134+
}
135+
}
136+
if (inlinedState.size !== 0) {
137+
for (const operand of eachTerminalOperand(block.terminal)) {
138+
inlinedState.delete(operand.identifier.id);
139+
}
140+
}
141+
}
142+
for (const block of fn.body.blocks.values()) {
143+
for (const instr of block.instructions) {
144+
const {value} = instr;
145+
switch (value.kind) {
146+
case 'FunctionExpression': {
147+
if (hasKnownNonRenderCall(value.loweredFunc.func)) {
148+
instr.value = {
149+
kind: 'Primitive',
150+
value: undefined,
151+
loc: value.loc,
152+
};
153+
}
154+
break;
155+
}
156+
case 'JsxExpression': {
157+
if (
158+
value.tag.kind === 'BuiltinTag' &&
159+
value.tag.name.indexOf('-') === -1
160+
) {
161+
const tag = value.tag.name;
162+
retainWhere(value.props, prop => {
163+
return (
164+
prop.kind === 'JsxSpreadAttribute' ||
165+
(!isKnownEventHandler(tag, prop.name) && prop.name !== 'ref')
166+
);
167+
});
168+
}
169+
break;
170+
}
171+
case 'Destructure': {
172+
if (inlinedState.has(value.value.identifier.id)) {
173+
// Canonical check is part of determining if state can inline, this is for TS
174+
CompilerError.invariant(
175+
value.lvalue.pattern.kind === 'ArrayPattern' &&
176+
value.lvalue.pattern.items.length >= 1 &&
177+
value.lvalue.pattern.items[0].kind === 'Identifier',
178+
{
179+
reason:
180+
'Expected a valid destructuring pattern for inlined state',
181+
description: null,
182+
details: [
183+
{
184+
kind: 'error',
185+
message: 'Expected a valid destructuring pattern',
186+
loc: value.loc,
187+
},
188+
],
189+
},
190+
);
191+
const store: StoreLocal = {
192+
kind: 'StoreLocal',
193+
loc: value.loc,
194+
type: null,
195+
lvalue: {
196+
kind: value.lvalue.kind,
197+
place: value.lvalue.pattern.items[0],
198+
},
199+
value: value.value,
200+
};
201+
instr.value = store;
202+
}
203+
break;
204+
}
205+
case 'MethodCall':
206+
case 'CallExpression': {
207+
const calleee =
208+
value.kind === 'CallExpression' ? value.callee : value.property;
209+
const hookKind = getHookKind(fn.env, calleee.identifier);
210+
switch (hookKind) {
211+
case 'useEffectEvent': {
212+
if (
213+
value.args.length === 1 &&
214+
value.args[0].kind === 'Identifier'
215+
) {
216+
const load: LoadLocal = {
217+
kind: 'LoadLocal',
218+
place: value.args[0],
219+
loc: value.loc,
220+
};
221+
instr.value = load;
222+
}
223+
break;
224+
}
225+
case 'useEffect':
226+
case 'useLayoutEffect':
227+
case 'useInsertionEffect': {
228+
// Drop effects
229+
instr.value = {
230+
kind: 'Primitive',
231+
value: undefined,
232+
loc: value.loc,
233+
};
234+
break;
235+
}
236+
case 'useReducer':
237+
case 'useState': {
238+
const replace = inlinedState.get(instr.lvalue.identifier.id);
239+
if (replace != null) {
240+
instr.value = replace;
241+
}
242+
break;
243+
}
244+
}
245+
}
246+
}
247+
}
248+
}
249+
}
250+
251+
function hasKnownNonRenderCall(fn: HIRFunction): boolean {
252+
for (const block of fn.body.blocks.values()) {
253+
for (const instr of block.instructions) {
254+
if (
255+
instr.value.kind === 'CallExpression' &&
256+
(isSetStateType(instr.value.callee.identifier) ||
257+
isStartTransitionType(instr.value.callee.identifier))
258+
) {
259+
return true;
260+
}
261+
}
262+
}
263+
return false;
264+
}
265+
266+
const EVENT_HANDLER_PATTERN = /^on[A-Z]/;
267+
function isKnownEventHandler(_tag: string, prop: string): boolean {
268+
return EVENT_HANDLER_PATTERN.test(prop);
269+
}

0 commit comments

Comments
 (0)