Skip to content

Commit 9d5ceb9

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 100fc4a commit 9d5ceb9

File tree

7 files changed

+216
-4
lines changed

7 files changed

+216
-4
lines changed

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

Lines changed: 7 additions & 1 deletion
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

109110
export type CompilerPipelineValue =
110111
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -236,6 +237,11 @@ function runWithEnvironment(
236237
}
237238
}
238239

240+
if (env.config.enableAllowSetStateFromRefsInEffects) {
241+
optimizeForSSR(hir);
242+
log({kind: 'hir', name: 'OptimizeForSSR', value: hir});
243+
}
244+
239245
// Note: Has to come after infer reference effects because "dead" code may still affect inference
240246
deadCodeElimination(hir);
241247
log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
@@ -305,7 +311,7 @@ function runWithEnvironment(
305311
value: hir,
306312
});
307313

308-
if (env.isInferredMemoEnabled) {
314+
if (env.isInferredMemoEnabled && !env.config.enableOptimizeForSSR) {
309315
if (env.config.validateStaticComponents) {
310316
env.logErrors(validateStaticComponents(hir));
311317
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,8 @@ export const EnvironmentConfigSchema = z.object({
670670
* from refs need to be stored in state during mount.
671671
*/
672672
enableAllowSetStateFromRefsInEffects: z.boolean().default(true),
673+
674+
enableOptimizeForSSR: z.boolean().default(false),
673675
});
674676

675677
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: 21 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,23 @@ 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+
const calleee =
323+
value.kind === 'CallExpression' ? value.callee : value.property;
324+
const hookKind = getHookKind(state.env, calleee.identifier);
325+
switch (hookKind) {
326+
case 'useRef': {
327+
// unused refs can be removed
328+
return true;
329+
}
330+
}
331+
return false;
332+
}
333+
case 'Await':
315334
case 'ComputedDelete':
316335
case 'ComputedStore':
317336
case 'PropertyDelete':
318-
case 'MethodCall':
319337
case 'PropertyStore':
320338
case 'StoreGlobal': {
321339
/*
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
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 {traverse} from '@babel/types';
9+
import {
10+
getHookKind,
11+
HIRFunction,
12+
IdentifierId,
13+
isArrayType,
14+
isObjectType,
15+
isPlainObjectType,
16+
isPrimitiveType,
17+
isUseStateType,
18+
LoadLocal,
19+
StoreLocal,
20+
} from '../HIR';
21+
import {
22+
eachInstructionValueOperand,
23+
eachTerminalOperand,
24+
} from '../HIR/visitors';
25+
import {retainWhere} from '../Utils/utils';
26+
27+
/**
28+
* Optimizes the code for running specifically in an SSR environment. This optimization
29+
* asssumes that setState will not be called during render during initial mount, which
30+
* allows inlining useState/useReducer.
31+
*
32+
* Optimizations:
33+
* - Inline useState/useReducer
34+
* - Inline useMemo/useCallback (happens in earlier passes)
35+
* - Remove effects
36+
* - Remove refs where known to be unused during render (eg directly passed to a dom node)
37+
* - Remove event handlers
38+
*/
39+
export function optimizeForSSR(fn: HIRFunction): void {
40+
const inlinedState = new Set<IdentifierId>();
41+
for (const block of fn.body.blocks.values()) {
42+
for (const instr of block.instructions) {
43+
const {value} = instr;
44+
switch (value.kind) {
45+
case 'JsxExpression': {
46+
if (
47+
value.tag.kind === 'BuiltinTag' &&
48+
value.tag.name.indexOf('-') === -1
49+
) {
50+
retainWhere(value.props, prop => {
51+
return (
52+
prop.kind === 'JsxSpreadAttribute' ||
53+
(prop.name !== 'ref' && !prop.name.startsWith('on'))
54+
);
55+
});
56+
}
57+
break;
58+
}
59+
case 'Destructure': {
60+
if (
61+
isUseStateType(value.value.identifier) &&
62+
inlinedState.has(value.value.identifier.id) &&
63+
value.lvalue.pattern.kind === 'ArrayPattern' &&
64+
value.lvalue.pattern.items.length >= 1 &&
65+
value.lvalue.pattern.items[0].kind === 'Identifier'
66+
) {
67+
const store: StoreLocal = {
68+
kind: 'StoreLocal',
69+
loc: value.loc,
70+
type: null,
71+
lvalue: {
72+
kind: value.lvalue.kind,
73+
place: value.lvalue.pattern.items[0],
74+
},
75+
value: value.value,
76+
};
77+
instr.value = store;
78+
}
79+
break;
80+
}
81+
case 'MethodCall':
82+
case 'CallExpression': {
83+
const calleee =
84+
value.kind === 'CallExpression' ? value.callee : value.property;
85+
const hookKind = getHookKind(fn.env, calleee.identifier);
86+
switch (hookKind) {
87+
case 'useEffectEvent': {
88+
if (
89+
value.args.length === 1 &&
90+
value.args[0].kind === 'Identifier'
91+
) {
92+
const load: LoadLocal = {
93+
kind: 'LoadLocal',
94+
place: value.args[0],
95+
loc: value.loc,
96+
};
97+
instr.value = load;
98+
}
99+
break;
100+
}
101+
case 'useEffect':
102+
case 'useLayoutEffect':
103+
case 'useInsertionEffect': {
104+
// Drop effects
105+
instr.value = {
106+
kind: 'Primitive',
107+
value: undefined,
108+
loc: value.loc,
109+
};
110+
break;
111+
}
112+
case 'useState': {
113+
if (
114+
value.args.length === 1 &&
115+
value.args[0].kind === 'Identifier'
116+
) {
117+
const arg = value.args[0];
118+
if (
119+
isPrimitiveType(arg.identifier) ||
120+
isPlainObjectType(arg.identifier) ||
121+
isArrayType(arg.identifier)
122+
) {
123+
instr.value = {
124+
kind: 'LoadLocal',
125+
place: arg,
126+
loc: arg.loc,
127+
};
128+
inlinedState.add(instr.lvalue.identifier.id);
129+
}
130+
}
131+
break;
132+
}
133+
}
134+
}
135+
}
136+
}
137+
}
138+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @enableOptimizeForSSR
6+
function Component() {
7+
const [state, setState] = useState(0);
8+
const ref = useRef(null);
9+
const onChange = e => {
10+
setState(e.target.value);
11+
};
12+
useEffect(() => {
13+
log(ref.current.value);
14+
});
15+
return <input value={state} onChange={onChange} ref={ref} />;
16+
}
17+
18+
```
19+
20+
## Code
21+
22+
```javascript
23+
// @enableOptimizeForSSR
24+
function Component() {
25+
const state = 0;
26+
return <input value={state} />;
27+
}
28+
29+
```
30+
31+
### Eval output
32+
(kind: exception) Fixture not implemented
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// @enableOptimizeForSSR
2+
function Component() {
3+
const [state, setState] = useState(0);
4+
const ref = useRef(null);
5+
const onChange = e => {
6+
setState(e.target.value);
7+
};
8+
useEffect(() => {
9+
log(ref.current.value);
10+
});
11+
return <input value={state} onChange={onChange} ref={ref} />;
12+
}

0 commit comments

Comments
 (0)