@samitouri / QOS-React-2 / commits / 4cf770d7e1

[compiler][poc] Quick experiment with SSR-optimization pass (#35102)

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. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35102). * #35112 * __->__ #35102

Joseph Savona committed Nov 20, 2025 at 15:02 UTC 4cf770d7e1a52c66401b42c7d135f40b7dc23981
16 files changed +576 -5
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts
+10 -2
@@ -105,6 +105,7 @@ import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRan
105 import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
106 import {validateNoDerivedComputationsInEffects_exp} from '../Validation/ValidateNoDerivedComputationsInEffects_exp';
107 import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
108 +import {optimizeForSSR} from '../Optimization/OptimizeForSSR';
109 import {validateSourceLocations} from '../Validation/ValidateSourceLocations';
110
111 export type CompilerPipelineValue =
@@ -237,6 +238,11 @@ function runWithEnvironment(
238 }
239 }
240
241 + if (env.config.enableOptimizeForSSR) {
242 + optimizeForSSR(hir);
243 + log({kind: 'hir', name: 'OptimizeForSSR', value: hir});
244 + }
245 +
246 // Note: Has to come after infer reference effects because "dead" code may still affect inference
247 deadCodeElimination(hir);
248 log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
@@ -314,8 +320,10 @@ function runWithEnvironment(
320 * if inferred memoization is enabled. This makes all later passes which
321 * transform reactive-scope labeled instructions no-ops.
322 */
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 + }
327 }
328
329 const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+2
@@ -691,6 +691,8 @@ export const EnvironmentConfigSchema = z.object({
691 * by React to only execute in response to events, not during render.
692 */
693 enableInferEventHandlers: z.boolean().default(false),
694 +
695 + enableOptimizeForSSR: z.boolean().default(false),
696 });
697
698 export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
+4
@@ -1823,6 +1823,10 @@ export function isPrimitiveType(id: Identifier): boolean {
1823 return id.type.kind === 'Primitive';
1824 }
1825
1826 +export function isPlainObjectType(id: Identifier): boolean {
1827 + return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInObject';
1828 +}
1829 +
1830 export function isArrayType(id: Identifier): boolean {
1831 return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInArray';
1832 }
compiler/packages/babel-plugin-react-compiler/src/Optimization/DeadCodeElimination.ts
+25 -3
@@ -7,6 +7,8 @@
7
8 import {
9 BlockId,
10 + Environment,
11 + getHookKind,
12 HIRFunction,
13 Identifier,
14 IdentifierId,
@@ -68,9 +70,14 @@ export function deadCodeElimination(fn: HIRFunction): void {
70 }
71
72 class State {
73 + env: Environment;
74 named: Set<string> = new Set();
75 identifiers: Set<IdentifierId> = new Set();
76
77 + constructor(env: Environment) {
78 + this.env = env;
79 + }
80 +
81 // Mark the identifier as being referenced (not dead code)
82 reference(identifier: Identifier): void {
83 this.identifiers.add(identifier.id);
@@ -112,7 +119,7 @@ function findReferencedIdentifiers(fn: HIRFunction): State {
119 const hasLoop = hasBackEdge(fn);
120 const reversedBlocks = [...fn.body.blocks.values()].reverse();
121
115 - const state = new State();
122 + const state = new State(fn.env);
123 let size = state.count;
124 do {
125 size = state.count;
@@ -310,12 +317,27 @@ function pruneableValue(value: InstructionValue, state: State): boolean {
317 // explicitly retain debugger statements to not break debugging workflows
318 return false;
319 }
313 - case 'Await':
320 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':
338 case 'ComputedDelete':
339 case 'ComputedStore':
340 case 'PropertyDelete':
318 - case 'MethodCall':
341 case 'PropertyStore':
342 case 'StoreGlobal': {
343 /*
compiler/packages/babel-plugin-react-compiler/src/Optimization/OptimizeForSSR.ts new
+269
@@ -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 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/optimize-ssr.expect.md new
+30
@@ -0,0 +1,30 @@
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 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/optimize-ssr.js new
+12
@@ -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 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-setState.expect.md new
+36
@@ -0,0 +1,36 @@
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 + // The known setState call allows us to infer this as an event handler
11 + // and prune it
12 + setState(e.target.value);
13 + };
14 + useEffect(() => {
15 + log(ref.current.value);
16 + });
17 + return <CustomInput value={state} onChange={onChange} ref={ref} />;
18 +}
19 +
20 +```
21 +
22 +## Code
23 +
24 +```javascript
25 +// @enableOptimizeForSSR
26 +function Component() {
27 + const state = 0;
28 + const ref = useRef(null);
29 + const onChange = undefined;
30 + return <CustomInput value={state} onChange={onChange} ref={ref} />;
31 +}
32 +
33 +```
34 +
35 +### Eval output
36 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-setState.js new
+14
@@ -0,0 +1,14 @@
1 +// @enableOptimizeForSSR
2 +function Component() {
3 + const [state, setState] = useState(0);
4 + const ref = useRef(null);
5 + const onChange = e => {
6 + // The known setState call allows us to infer this as an event handler
7 + // and prune it
8 + setState(e.target.value);
9 + };
10 + useEffect(() => {
11 + log(ref.current.value);
12 + });
13 + return <CustomInput value={state} onChange={onChange} ref={ref} />;
14 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-startTransition.expect.md new
+40
@@ -0,0 +1,40 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableOptimizeForSSR
6 +function Component() {
7 + const [, startTransition] = useTransition();
8 + const [state, setState] = useState(0);
9 + const ref = useRef(null);
10 + const onChange = e => {
11 + // The known startTransition call allows us to infer this as an event handler
12 + // and prune it
13 + startTransition(() => {
14 + setState.call(null, e.target.value);
15 + });
16 + };
17 + useEffect(() => {
18 + log(ref.current.value);
19 + });
20 + return <CustomInput value={state} onChange={onChange} ref={ref} />;
21 +}
22 +
23 +```
24 +
25 +## Code
26 +
27 +```javascript
28 +// @enableOptimizeForSSR
29 +function Component() {
30 + useTransition();
31 + const state = 0;
32 + const ref = useRef(null);
33 + const onChange = undefined;
34 + return <CustomInput value={state} onChange={onChange} ref={ref} />;
35 +}
36 +
37 +```
38 +
39 +### Eval output
40 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-infer-event-handlers-from-startTransition.js new
+17
@@ -0,0 +1,17 @@
1 +// @enableOptimizeForSSR
2 +function Component() {
3 + const [, startTransition] = useTransition();
4 + const [state, setState] = useState(0);
5 + const ref = useRef(null);
6 + const onChange = e => {
7 + // The known startTransition call allows us to infer this as an event handler
8 + // and prune it
9 + startTransition(() => {
10 + setState.call(null, e.target.value);
11 + });
12 + };
13 + useEffect(() => {
14 + log(ref.current.value);
15 + });
16 + return <CustomInput value={state} onChange={onChange} ref={ref} />;
17 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer-initializer.expect.md new
+42
@@ -0,0 +1,42 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableOptimizeForSSR
6 +
7 +import {useReducer} from 'react';
8 +
9 +const initializer = x => x;
10 +
11 +function Component() {
12 + const [state, dispatch] = useReducer((_, next) => next, 0, initializer);
13 + const ref = useRef(null);
14 + const onChange = e => {
15 + dispatch(e.target.value);
16 + };
17 + useEffect(() => {
18 + log(ref.current.value);
19 + });
20 + return <input value={state} onChange={onChange} ref={ref} />;
21 +}
22 +
23 +```
24 +
25 +## Code
26 +
27 +```javascript
28 +// @enableOptimizeForSSR
29 +
30 +import { useReducer } from "react";
31 +
32 +const initializer = (x) => {
33 + return x;
34 +};
35 +
36 +function Component() {
37 + const state = initializer(0);
38 + return <input value={state} />;
39 +}
40 +
41 +```
42 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer-initializer.js new
+17
@@ -0,0 +1,17 @@
1 +// @enableOptimizeForSSR
2 +
3 +import {useReducer} from 'react';
4 +
5 +const initializer = x => x;
6 +
7 +function Component() {
8 + const [state, dispatch] = useReducer((_, next) => next, 0, initializer);
9 + const ref = useRef(null);
10 + const onChange = e => {
11 + dispatch(e.target.value);
12 + };
13 + useEffect(() => {
14 + log(ref.current.value);
15 + });
16 + return <input value={state} onChange={onChange} ref={ref} />;
17 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer.expect.md new
+36
@@ -0,0 +1,36 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableOptimizeForSSR
6 +
7 +import {useReducer} from 'react';
8 +
9 +function Component() {
10 + const [state, dispatch] = useReducer((_, next) => next, 0);
11 + const ref = useRef(null);
12 + const onChange = e => {
13 + dispatch(e.target.value);
14 + };
15 + useEffect(() => {
16 + log(ref.current.value);
17 + });
18 + return <input value={state} onChange={onChange} ref={ref} />;
19 +}
20 +
21 +```
22 +
23 +## Code
24 +
25 +```javascript
26 +// @enableOptimizeForSSR
27 +
28 +import { useReducer } from "react";
29 +
30 +function Component() {
31 + const state = 0;
32 + return <input value={state} />;
33 +}
34 +
35 +```
36 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssr/ssr-use-reducer.js new
+15
@@ -0,0 +1,15 @@
1 +// @enableOptimizeForSSR
2 +
3 +import {useReducer} from 'react';
4 +
5 +function Component() {
6 + const [state, dispatch] = useReducer((_, next) => next, 0);
7 + const ref = useRef(null);
8 + const onChange = e => {
9 + dispatch(e.target.value);
10 + };
11 + useEffect(() => {
12 + log(ref.current.value);
13 + });
14 + return <input value={state} onChange={onChange} ref={ref} />;
15 +}
compiler/packages/snap/src/SproutTodoFilter.ts
+7
@@ -487,6 +487,13 @@ const skipFilter = new Set([
487 'lower-context-selector-simple',
488 'lower-context-acess-multiple',
489 'bug-separate-memoization-due-to-callback-capturing',
490 +
491 + // SSR optimization rewrites files in a way that causes differences or warnings
492 + 'ssr/optimize-ssr',
493 + 'ssr/ssr-use-reducer',
494 + 'ssr/ssr-use-reducer-initializer',
495 + 'ssr/infer-event-handlers-from-setState',
496 + 'ssr/infer-event-handlers-from-startTransition',
497 ]);
498
499 export default skipFilter;