Skip to content

Commit 84c1d4b

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 84c1d4b

File tree

7 files changed

+213
-4
lines changed

7 files changed

+213
-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: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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+
for (const block of fn.body.blocks.values()) {
41+
for (const instr of block.instructions) {
42+
const {value} = instr;
43+
switch (value.kind) {
44+
case 'JsxExpression': {
45+
if (
46+
value.tag.kind === 'BuiltinTag' &&
47+
value.tag.name.indexOf('-') === -1
48+
) {
49+
retainWhere(value.props, prop => {
50+
return (
51+
prop.kind === 'JsxSpreadAttribute' ||
52+
(prop.name !== 'ref' && !prop.name.startsWith('on'))
53+
);
54+
});
55+
}
56+
break;
57+
}
58+
case 'Destructure': {
59+
if (
60+
isUseStateType(value.value.identifier) &&
61+
value.lvalue.pattern.kind === 'ArrayPattern' &&
62+
value.lvalue.pattern.items.length >= 1 &&
63+
value.lvalue.pattern.items[0].kind === 'Identifier'
64+
) {
65+
const store: StoreLocal = {
66+
kind: 'StoreLocal',
67+
loc: value.loc,
68+
type: null,
69+
lvalue: {
70+
kind: value.lvalue.kind,
71+
place: value.lvalue.pattern.items[0],
72+
},
73+
value: value.value,
74+
};
75+
instr.value = store;
76+
}
77+
break;
78+
}
79+
case 'MethodCall':
80+
case 'CallExpression': {
81+
const calleee =
82+
value.kind === 'CallExpression' ? value.callee : value.property;
83+
const hookKind = getHookKind(fn.env, calleee.identifier);
84+
switch (hookKind) {
85+
case 'useEffectEvent': {
86+
if (
87+
value.args.length === 1 &&
88+
value.args[0].kind === 'Identifier'
89+
) {
90+
const load: LoadLocal = {
91+
kind: 'LoadLocal',
92+
place: value.args[0],
93+
loc: value.loc,
94+
};
95+
instr.value = load;
96+
}
97+
break;
98+
}
99+
case 'useEffect':
100+
case 'useLayoutEffect':
101+
case 'useInsertionEffect': {
102+
// Drop effects
103+
instr.value = {
104+
kind: 'Primitive',
105+
value: undefined,
106+
loc: value.loc,
107+
};
108+
break;
109+
}
110+
case 'useState': {
111+
if (
112+
value.args.length === 1 &&
113+
value.args[0].kind === 'Identifier'
114+
) {
115+
const arg = value.args[0];
116+
if (
117+
isPrimitiveType(arg.identifier) ||
118+
isPlainObjectType(arg.identifier) ||
119+
isArrayType(arg.identifier)
120+
) {
121+
instr.value = {
122+
kind: 'LoadLocal',
123+
place: arg,
124+
loc: arg.loc,
125+
};
126+
}
127+
}
128+
break;
129+
}
130+
}
131+
}
132+
}
133+
}
134+
}
135+
}
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)