Autogenerated summaries of each of the compiler passes which allow agents to get the key ideas of a compiler pass, including key input/output invariants, without having to reprocess the file each time. In the subsequent diff this seemed to help. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35595). * #35607 * #35298 * #35596 * #35573 * __->__ #35595 * #35539
4.9 KiB
optimizeForSSR
File
src/Optimization/OptimizeForSSR.ts
Purpose
This pass applies Server-Side Rendering (SSR) specific optimizations. During SSR, React renders components to HTML strings without mounting them in the DOM. This means:
- Effects don't run -
useEffectanduseLayoutEffectare no-ops - Event handlers aren't needed - There's no DOM to attach handlers to
- State is never updated - Components render once with initial state
- Refs aren't attached - There's no DOM to ref
The pass leverages these SSR characteristics to inline and simplify code, removing unnecessary runtime overhead.
Input Invariants
- The function has been through type inference
- Hook types are properly identified (useState, useReducer, useEffect, etc.)
- Function types for callbacks are properly inferred
Output Guarantees
useState(initialValue)is inlined to just[initialValue, noop]useReducer(reducer, initialArg, init?)is inlined to[init ? init(initialArg) : initialArg, noop]useEffectanduseLayoutEffectcalls are removed entirely- Event handler functions (functions that call setState) are replaced with empty functions
- Ref-typed values are removed from JSX props
Algorithm
Phase 1: Identify Inlinable State
const inlinedState = new Map<IdentifierId, InstructionValue>();
for (const instr of block.instructions) {
if (isUseStateCall(instr)) {
// Store the initial value for inlining
inlinedState.set(instr.lvalue.id, {
kind: 'ArrayExpression',
elements: [initialValue, noopFunction],
});
}
if (isUseReducerCall(instr)) {
// Compute initial state and store for inlining
const initialState = init ? callInit(initialArg) : initialArg;
inlinedState.set(instr.lvalue.id, {
kind: 'ArrayExpression',
elements: [initialState, noopFunction],
});
}
}
Phase 2: Inline State Hooks
Replace useState/useReducer with their computed initial values:
// Before:
$0 = useState(0)
[state, setState] = $0
// After (inlined):
$0 = [0, () => {}]
[state, setState] = $0
Phase 3: Remove Effects
if (isUseEffectCall(instr) || isUseLayoutEffectCall(instr)) {
// Remove the instruction entirely
block.instructions.splice(i, 1);
}
Phase 4: Identify and Neuter Event Handlers
// Functions that capture and call setState are event handlers
if (capturesSetState(functionExpr)) {
// Replace with empty function
instr.value = {
kind: 'FunctionExpression',
params: originalParams,
body: emptyBody,
};
}
Phase 5: Remove Ref Props
if (isJSX(instr) && hasRefProp(instr)) {
// Remove ref={...} from JSX props
removeRefProp(instr.value);
}
Edge Cases
useState with Function Initializer
When useState receives a function initializer, it must be called:
// useState(() => expensive())
// SSR: Call the initializer to get the value
const [state] = [expensiveComputation(), noop];
useReducer with Init Function
The optional init function is called with initialArg:
// useReducer(reducer, arg, init)
// SSR: [init(arg), noop]
Nested State Setters
Functions that transitively call setState are also event handlers:
function outer() {
function inner() {
setState(x); // inner is event handler
}
inner(); // outer is also event handler
}
Conditional Event Handlers
Event handler detection is conservative - if a function might call setState, it's treated as an event handler.
Refs in Nested Objects
Only direct ref props on JSX are removed:
<div ref={myRef} /> // ref removed
<div config={{ref: myRef}} /> // ref NOT removed (nested)
TODOs
None in the source file.
Example
Fixture: ssr/optimize-ssr.js
Input:
function Component() {
const [state, setState] = useState(0);
const ref = useRef(null);
const onChange = (e) => {
setState(e.target.value);
};
useEffect(() => {
log(ref.current.value);
});
return <input value={state} onChange={onChange} ref={ref} />;
}
After SSR Optimization:
function Component() {
const $ = _c(1);
// useState inlined to [initialValue, noop]
const [state] = [0, () => {}];
// useRef returns object with current: null
const ref = { current: null };
// Event handler replaced with noop (it calls setState)
const onChange = () => {};
// useEffect removed entirely (no-op on SSR)
// ref prop removed from JSX
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <input value={state} onChange={onChange} />;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
Key observations:
useState(0)becomes[0, () => {}]- no hook calluseEffect(...)is removed entirelyonChangeis replaced with empty function since it calledsetStateref={ref}prop is removed from JSX- SSR output is simpler and has less runtime overhead