Skip to content

Commit a088349

Browse files
committed
[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 DiffTrain build for [4cf770d](4cf770d)
1 parent 3e27852 commit a088349

35 files changed

+306
-92
lines changed

compiled/eslint-plugin-react-hooks/index.js

Lines changed: 220 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18928,6 +18928,9 @@ function isObjectMethodType(id) {
1892818928
function isPrimitiveType(id) {
1892918929
return id.type.kind === 'Primitive';
1893018930
}
18931+
function isPlainObjectType(id) {
18932+
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInObject';
18933+
}
1893118934
function isArrayType(id) {
1893218935
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInArray';
1893318936
}
@@ -32263,6 +32266,7 @@ const EnvironmentConfigSchema = v4.z.object({
3226332266
validateNoDynamicallyCreatedComponentsOrHooks: v4.z.boolean().default(false),
3226432267
enableAllowSetStateFromRefsInEffects: v4.z.boolean().default(true),
3226532268
enableInferEventHandlers: v4.z.boolean().default(false),
32269+
enableOptimizeForSSR: v4.z.boolean().default(false),
3226632270
});
3226732271
class Environment {
3226832272
constructor(scope, fnType, compilerMode, config, contextIdentifiers, parentFunction, logger, filename, code, programContext) {
@@ -34479,9 +34483,10 @@ function deadCodeElimination(fn) {
3447934483
retainWhere(fn.context, contextVar => state.isIdOrNameUsed(contextVar.identifier));
3448034484
}
3448134485
let State$2 = class State {
34482-
constructor() {
34486+
constructor(env) {
3448334487
this.named = new Set();
3448434488
this.identifiers = new Set();
34489+
this.env = env;
3448534490
}
3448634491
reference(identifier) {
3448734492
this.identifiers.add(identifier.id);
@@ -34503,7 +34508,7 @@ let State$2 = class State {
3450334508
function findReferencedIdentifiers(fn) {
3450434509
const hasLoop = hasBackEdge(fn);
3450534510
const reversedBlocks = [...fn.body.blocks.values()].reverse();
34506-
const state = new State$2();
34511+
const state = new State$2(fn.env);
3450734512
let size = state.count;
3450834513
do {
3450934514
size = state.count;
@@ -34650,12 +34655,25 @@ function pruneableValue(value, state) {
3465034655
case 'Debugger': {
3465134656
return false;
3465234657
}
34653-
case 'Await':
3465434658
case 'CallExpression':
34659+
case 'MethodCall': {
34660+
if (state.env.config.enableOptimizeForSSR) {
34661+
const calleee = value.kind === 'CallExpression' ? value.callee : value.property;
34662+
const hookKind = getHookKind(state.env, calleee.identifier);
34663+
switch (hookKind) {
34664+
case 'useState':
34665+
case 'useReducer':
34666+
case 'useRef': {
34667+
return true;
34668+
}
34669+
}
34670+
}
34671+
return false;
34672+
}
34673+
case 'Await':
3465534674
case 'ComputedDelete':
3465634675
case 'ComputedStore':
3465734676
case 'PropertyDelete':
34658-
case 'MethodCall':
3465934677
case 'PropertyStore':
3466034678
case 'StoreGlobal': {
3466134679
return false;
@@ -53182,6 +53200,196 @@ function nameAnonymousFunctionsImpl(fn) {
5318253200
return nodes;
5318353201
}
5318453202

53203+
function optimizeForSSR(fn) {
53204+
const inlinedState = new Map();
53205+
for (const block of fn.body.blocks.values()) {
53206+
for (const instr of block.instructions) {
53207+
const { value } = instr;
53208+
switch (value.kind) {
53209+
case 'Destructure': {
53210+
if (inlinedState.has(value.value.identifier.id) &&
53211+
value.lvalue.pattern.kind === 'ArrayPattern' &&
53212+
value.lvalue.pattern.items.length >= 1 &&
53213+
value.lvalue.pattern.items[0].kind === 'Identifier') {
53214+
continue;
53215+
}
53216+
break;
53217+
}
53218+
case 'MethodCall':
53219+
case 'CallExpression': {
53220+
const calleee = value.kind === 'CallExpression' ? value.callee : value.property;
53221+
const hookKind = getHookKind(fn.env, calleee.identifier);
53222+
switch (hookKind) {
53223+
case 'useReducer': {
53224+
if (value.args.length === 2 &&
53225+
value.args[1].kind === 'Identifier') {
53226+
const arg = value.args[1];
53227+
const replace = {
53228+
kind: 'LoadLocal',
53229+
place: arg,
53230+
loc: arg.loc,
53231+
};
53232+
inlinedState.set(instr.lvalue.identifier.id, replace);
53233+
}
53234+
else if (value.args.length === 3 &&
53235+
value.args[1].kind === 'Identifier' &&
53236+
value.args[2].kind === 'Identifier') {
53237+
const arg = value.args[1];
53238+
const initializer = value.args[2];
53239+
const replace = {
53240+
kind: 'CallExpression',
53241+
callee: initializer,
53242+
args: [arg],
53243+
loc: value.loc,
53244+
};
53245+
inlinedState.set(instr.lvalue.identifier.id, replace);
53246+
}
53247+
break;
53248+
}
53249+
case 'useState': {
53250+
if (value.args.length === 1 &&
53251+
value.args[0].kind === 'Identifier') {
53252+
const arg = value.args[0];
53253+
if (isPrimitiveType(arg.identifier) ||
53254+
isPlainObjectType(arg.identifier) ||
53255+
isArrayType(arg.identifier)) {
53256+
const replace = {
53257+
kind: 'LoadLocal',
53258+
place: arg,
53259+
loc: arg.loc,
53260+
};
53261+
inlinedState.set(instr.lvalue.identifier.id, replace);
53262+
}
53263+
}
53264+
break;
53265+
}
53266+
}
53267+
}
53268+
}
53269+
if (inlinedState.size !== 0) {
53270+
for (const operand of eachInstructionValueOperand(value)) {
53271+
inlinedState.delete(operand.identifier.id);
53272+
}
53273+
}
53274+
}
53275+
if (inlinedState.size !== 0) {
53276+
for (const operand of eachTerminalOperand(block.terminal)) {
53277+
inlinedState.delete(operand.identifier.id);
53278+
}
53279+
}
53280+
}
53281+
for (const block of fn.body.blocks.values()) {
53282+
for (const instr of block.instructions) {
53283+
const { value } = instr;
53284+
switch (value.kind) {
53285+
case 'FunctionExpression': {
53286+
if (hasKnownNonRenderCall(value.loweredFunc.func)) {
53287+
instr.value = {
53288+
kind: 'Primitive',
53289+
value: undefined,
53290+
loc: value.loc,
53291+
};
53292+
}
53293+
break;
53294+
}
53295+
case 'JsxExpression': {
53296+
if (value.tag.kind === 'BuiltinTag' &&
53297+
value.tag.name.indexOf('-') === -1) {
53298+
const tag = value.tag.name;
53299+
retainWhere(value.props, prop => {
53300+
return (prop.kind === 'JsxSpreadAttribute' ||
53301+
(!isKnownEventHandler(tag, prop.name) && prop.name !== 'ref'));
53302+
});
53303+
}
53304+
break;
53305+
}
53306+
case 'Destructure': {
53307+
if (inlinedState.has(value.value.identifier.id)) {
53308+
CompilerError.invariant(value.lvalue.pattern.kind === 'ArrayPattern' &&
53309+
value.lvalue.pattern.items.length >= 1 &&
53310+
value.lvalue.pattern.items[0].kind === 'Identifier', {
53311+
reason: 'Expected a valid destructuring pattern for inlined state',
53312+
description: null,
53313+
details: [
53314+
{
53315+
kind: 'error',
53316+
message: 'Expected a valid destructuring pattern',
53317+
loc: value.loc,
53318+
},
53319+
],
53320+
});
53321+
const store = {
53322+
kind: 'StoreLocal',
53323+
loc: value.loc,
53324+
type: null,
53325+
lvalue: {
53326+
kind: value.lvalue.kind,
53327+
place: value.lvalue.pattern.items[0],
53328+
},
53329+
value: value.value,
53330+
};
53331+
instr.value = store;
53332+
}
53333+
break;
53334+
}
53335+
case 'MethodCall':
53336+
case 'CallExpression': {
53337+
const calleee = value.kind === 'CallExpression' ? value.callee : value.property;
53338+
const hookKind = getHookKind(fn.env, calleee.identifier);
53339+
switch (hookKind) {
53340+
case 'useEffectEvent': {
53341+
if (value.args.length === 1 &&
53342+
value.args[0].kind === 'Identifier') {
53343+
const load = {
53344+
kind: 'LoadLocal',
53345+
place: value.args[0],
53346+
loc: value.loc,
53347+
};
53348+
instr.value = load;
53349+
}
53350+
break;
53351+
}
53352+
case 'useEffect':
53353+
case 'useLayoutEffect':
53354+
case 'useInsertionEffect': {
53355+
instr.value = {
53356+
kind: 'Primitive',
53357+
value: undefined,
53358+
loc: value.loc,
53359+
};
53360+
break;
53361+
}
53362+
case 'useReducer':
53363+
case 'useState': {
53364+
const replace = inlinedState.get(instr.lvalue.identifier.id);
53365+
if (replace != null) {
53366+
instr.value = replace;
53367+
}
53368+
break;
53369+
}
53370+
}
53371+
}
53372+
}
53373+
}
53374+
}
53375+
}
53376+
function hasKnownNonRenderCall(fn) {
53377+
for (const block of fn.body.blocks.values()) {
53378+
for (const instr of block.instructions) {
53379+
if (instr.value.kind === 'CallExpression' &&
53380+
(isSetStateType(instr.value.callee.identifier) ||
53381+
isStartTransitionType(instr.value.callee.identifier))) {
53382+
return true;
53383+
}
53384+
}
53385+
}
53386+
return false;
53387+
}
53388+
const EVENT_HANDLER_PATTERN = /^on[A-Z]/;
53389+
function isKnownEventHandler(_tag, prop) {
53390+
return EVENT_HANDLER_PATTERN.test(prop);
53391+
}
53392+
5318553393
function run(func, config, fnType, mode, programContext, logger, filename, code) {
5318653394
var _a, _b;
5318753395
const contextIdentifiers = findContextIdentifiers(func);
@@ -53256,6 +53464,10 @@ function runWithEnvironment(func, env) {
5325653464
throw mutabilityAliasingErrors.unwrapErr();
5325753465
}
5325853466
}
53467+
if (env.config.enableOptimizeForSSR) {
53468+
optimizeForSSR(hir);
53469+
log({ kind: 'hir', name: 'OptimizeForSSR', value: hir });
53470+
}
5325953471
deadCodeElimination(hir);
5326053472
log({ kind: 'hir', name: 'DeadCodeElimination', value: hir });
5326153473
if (env.config.enableInstructionReordering) {
@@ -53313,8 +53525,10 @@ function runWithEnvironment(func, env) {
5331353525
if (env.config.validateStaticComponents) {
5331453526
env.logErrors(validateStaticComponents(hir));
5331553527
}
53316-
inferReactiveScopeVariables(hir);
53317-
log({ kind: 'hir', name: 'InferReactiveScopeVariables', value: hir });
53528+
if (!env.config.enableOptimizeForSSR) {
53529+
inferReactiveScopeVariables(hir);
53530+
log({ kind: 'hir', name: 'InferReactiveScopeVariables', value: hir });
53531+
}
5331853532
}
5331953533
const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
5332053534
log({

compiled/facebook-www/REVISION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
7d67591041e3ac47eac3bab6cff209071d4c0c6c
1+
4cf770d7e1a52c66401b42c7d135f40b7dc23981
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
7d67591041e3ac47eac3bab6cff209071d4c0c6c
1+
4cf770d7e1a52c66401b42c7d135f40b7dc23981

compiled/facebook-www/React-dev.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1499,7 +1499,7 @@ __DEV__ &&
14991499
exports.useTransition = function () {
15001500
return resolveDispatcher().useTransition();
15011501
};
1502-
exports.version = "19.3.0-www-classic-7d675910-20251120";
1502+
exports.version = "19.3.0-www-classic-4cf770d7-20251120";
15031503
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
15041504
"function" ===
15051505
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/React-dev.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1499,7 +1499,7 @@ __DEV__ &&
14991499
exports.useTransition = function () {
15001500
return resolveDispatcher().useTransition();
15011501
};
1502-
exports.version = "19.3.0-www-modern-7d675910-20251120";
1502+
exports.version = "19.3.0-www-modern-4cf770d7-20251120";
15031503
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
15041504
"function" ===
15051505
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/React-prod.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -606,4 +606,4 @@ exports.useSyncExternalStore = function (
606606
exports.useTransition = function () {
607607
return ReactSharedInternals.H.useTransition();
608608
};
609-
exports.version = "19.3.0-www-classic-7d675910-20251120";
609+
exports.version = "19.3.0-www-classic-4cf770d7-20251120";

compiled/facebook-www/React-prod.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -606,4 +606,4 @@ exports.useSyncExternalStore = function (
606606
exports.useTransition = function () {
607607
return ReactSharedInternals.H.useTransition();
608608
};
609-
exports.version = "19.3.0-www-modern-7d675910-20251120";
609+
exports.version = "19.3.0-www-modern-4cf770d7-20251120";

compiled/facebook-www/React-profiling.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,7 @@ exports.useSyncExternalStore = function (
610610
exports.useTransition = function () {
611611
return ReactSharedInternals.H.useTransition();
612612
};
613-
exports.version = "19.3.0-www-classic-7d675910-20251120";
613+
exports.version = "19.3.0-www-classic-4cf770d7-20251120";
614614
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
615615
"function" ===
616616
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/React-profiling.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,7 @@ exports.useSyncExternalStore = function (
610610
exports.useTransition = function () {
611611
return ReactSharedInternals.H.useTransition();
612612
};
613-
exports.version = "19.3.0-www-modern-7d675910-20251120";
613+
exports.version = "19.3.0-www-modern-4cf770d7-20251120";
614614
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
615615
"function" ===
616616
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/ReactART-dev.classic.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20468,10 +20468,10 @@ __DEV__ &&
2046820468
(function () {
2046920469
var internals = {
2047020470
bundleType: 1,
20471-
version: "19.3.0-www-classic-7d675910-20251120",
20471+
version: "19.3.0-www-classic-4cf770d7-20251120",
2047220472
rendererPackageName: "react-art",
2047320473
currentDispatcherRef: ReactSharedInternals,
20474-
reconcilerVersion: "19.3.0-www-classic-7d675910-20251120"
20474+
reconcilerVersion: "19.3.0-www-classic-4cf770d7-20251120"
2047520475
};
2047620476
internals.overrideHookState = overrideHookState;
2047720477
internals.overrideHookStateDeletePath = overrideHookStateDeletePath;
@@ -20506,7 +20506,7 @@ __DEV__ &&
2050620506
exports.Shape = Shape;
2050720507
exports.Surface = Surface;
2050820508
exports.Text = Text;
20509-
exports.version = "19.3.0-www-classic-7d675910-20251120";
20509+
exports.version = "19.3.0-www-classic-4cf770d7-20251120";
2051020510
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
2051120511
"function" ===
2051220512
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

0 commit comments

Comments
 (0)