[Flight/Fizz] Reset ThenableState Only in Branches Where It's Added (#28068)
Before, we used to reset the thenable state and extract the previous state very early so that it's only the retried task that can possibly consume it. This is nice because we can't accidentally consume that state for any other node. However, it does add a lot of branches of code that has to pass this around. It also adds extra bytes on the stack per node. Even though it's mostly just null. This changes it so that where ever we can create a thenable state (e.g. entering a component with hooks) we first extract this from the task. The principle is that whatever could've created the thenable state in the first place, must always be rerendered so it'll take the same code paths to get there and so we'll always consume it.
Sebastian Markbåge committed
Jan 25, 2024 at 19:52 UTC
382190c595126837ee7e43b4fa953f4edd30e01c
2 files changed
+48
-155
packages/react-server/src/ReactFizzServer.js
+29
-115
@@ -1294,11 +1294,15 @@ function renderWithHooks<Props, SecondArg>(
1294
request: Request,
1295
task: Task,
1296
keyPath: KeyNode,
1297
- prevThenableState: ThenableState | null,
1297
Component: (p: Props, arg: SecondArg) => any,
1298
props: Props,
1299
secondArg: SecondArg,
1300
): any {
1301
+ // Reset the task's thenable state before continuing, so that if a later
1302
+ // component suspends we can reuse the same task object. If the same
1303
+ // component suspends again, the thenable state will be restored.
1304
+ const prevThenableState = task.thenableState;
1305
+ task.thenableState = null;
1306
const componentIdentity = {};
1307
prepareToUseHooks(
1308
request,
@@ -1345,7 +1349,7 @@ function finishClassComponent(
1349
childContextTypes,
1350
);
1351
task.legacyContext = mergedContext;
1348
- renderNodeDestructive(request, task, null, nextChildren, -1);
1352
+ renderNodeDestructive(request, task, nextChildren, -1);
1353
task.legacyContext = previousContext;
1354
return;
1355
}
@@ -1353,7 +1357,7 @@ function finishClassComponent(
1357
1358
const prevKeyPath = task.keyPath;
1359
task.keyPath = keyPath;
1356
- renderNodeDestructive(request, task, null, nextChildren, -1);
1360
+ renderNodeDestructive(request, task, nextChildren, -1);
1361
task.keyPath = prevKeyPath;
1362
}
1363
@@ -1391,7 +1395,6 @@ function renderIndeterminateComponent(
1395
request: Request,
1396
task: Task,
1397
keyPath: KeyNode,
1394
- prevThenableState: ThenableState | null,
1398
Component: any,
1399
props: any,
1400
): void {
@@ -1425,7 +1428,6 @@ function renderIndeterminateComponent(
1428
request,
1429
task,
1430
keyPath,
1428
- prevThenableState,
1431
Component,
1432
props,
1433
legacyContext,
@@ -1569,7 +1571,7 @@ function finishFunctionComponent(
1571
// We're now successfully past this task, and we haven't modified the
1572
// context stack. We don't have to pop back to the previous task every
1573
// again, so we can use the destructive recursive form.
1572
- renderNodeDestructive(request, task, null, children, -1);
1574
+ renderNodeDestructive(request, task, children, -1);
1575
}
1576
task.keyPath = prevKeyPath;
1577
}
@@ -1646,7 +1648,6 @@ function renderForwardRef(
1648
request: Request,
1649
task: Task,
1650
keyPath: KeyNode,
1649
- prevThenableState: null | ThenableState,
1651
type: any,
1652
props: Object,
1653
ref: any,
@@ -1657,7 +1658,6 @@ function renderForwardRef(
1658
request,
1659
task,
1660
keyPath,
1660
- prevThenableState,
1661
type.render,
1662
props,
1663
ref,
@@ -1681,22 +1681,13 @@ function renderMemo(
1681
request: Request,
1682
task: Task,
1683
keyPath: KeyNode,
1684
- prevThenableState: ThenableState | null,
1684
type: any,
1685
props: Object,
1686
ref: any,
1687
): void {
1688
const innerType = type.type;
1689
const resolvedProps = resolveDefaultProps(innerType, props);
1691
- renderElement(
1692
- request,
1693
- task,
1694
- keyPath,
1695
- prevThenableState,
1696
- innerType,
1697
- resolvedProps,
1698
- ref,
1699
- );
1690
+ renderElement(request, task, keyPath, innerType, resolvedProps, ref);
1691
}
1692
1693
function renderContextConsumer(
@@ -1749,7 +1740,7 @@ function renderContextConsumer(
1740
1741
const prevKeyPath = task.keyPath;
1742
task.keyPath = keyPath;
1752
- renderNodeDestructive(request, task, null, newChildren, -1);
1743
+ renderNodeDestructive(request, task, newChildren, -1);
1744
task.keyPath = prevKeyPath;
1745
}
1746
@@ -1770,7 +1761,7 @@ function renderContextProvider(
1761
const prevKeyPath = task.keyPath;
1762
task.context = pushProvider(context, value);
1763
task.keyPath = keyPath;
1773
- renderNodeDestructive(request, task, null, children, -1);
1764
+ renderNodeDestructive(request, task, children, -1);
1765
task.context = popProvider(context);
1766
task.keyPath = prevKeyPath;
1767
if (__DEV__) {
@@ -1786,7 +1777,6 @@ function renderLazyComponent(
1777
request: Request,
1778
task: Task,
1779
keyPath: KeyNode,
1789
- prevThenableState: ThenableState | null,
1780
lazyComponent: LazyComponentType<any, any>,
1781
props: Object,
1782
ref: any,
@@ -1797,15 +1787,7 @@ function renderLazyComponent(
1787
const init = lazyComponent._init;
1788
const Component = init(payload);
1789
const resolvedProps = resolveDefaultProps(Component, props);
1800
- renderElement(
1801
- request,
1802
- task,
1803
- keyPath,
1804
- prevThenableState,
1805
- Component,
1806
- resolvedProps,
1807
- ref,
1808
- );
1790
+ renderElement(request, task, keyPath, Component, resolvedProps, ref);
1791
task.componentStack = previousComponentStack;
1792
}
1793
@@ -1824,7 +1806,7 @@ function renderOffscreen(
1806
// pure indirection.
1807
const prevKeyPath = task.keyPath;
1808
task.keyPath = keyPath;
1827
- renderNodeDestructive(request, task, null, props.children, -1);
1809
+ renderNodeDestructive(request, task, props.children, -1);
1810
task.keyPath = prevKeyPath;
1811
}
1812
}
@@ -1833,7 +1815,6 @@ function renderElement(
1815
request: Request,
1816
task: Task,
1817
keyPath: KeyNode,
1836
- prevThenableState: ThenableState | null,
1818
type: any,
1819
props: Object,
1820
ref: any,
@@ -1843,14 +1824,7 @@ function renderElement(
1824
renderClassComponent(request, task, keyPath, type, props);
1825
return;
1826
} else {
1846
- renderIndeterminateComponent(
1847
- request,
1848
- task,
1849
- keyPath,
1850
- prevThenableState,
1851
- type,
1852
- props,
1853
- );
1827
+ renderIndeterminateComponent(request, task, keyPath, type, props);
1828
return;
1829
}
1830
}
@@ -1876,7 +1850,7 @@ function renderElement(
1850
case REACT_FRAGMENT_TYPE: {
1851
const prevKeyPath = task.keyPath;
1852
task.keyPath = keyPath;
1879
- renderNodeDestructive(request, task, null, props.children, -1);
1853
+ renderNodeDestructive(request, task, props.children, -1);
1854
task.keyPath = prevKeyPath;
1855
return;
1856
}
@@ -1890,7 +1864,7 @@ function renderElement(
1864
// TODO: SuspenseList should control the boundaries.
1865
const prevKeyPath = task.keyPath;
1866
task.keyPath = keyPath;
1893
- renderNodeDestructive(request, task, null, props.children, -1);
1867
+ renderNodeDestructive(request, task, props.children, -1);
1868
task.keyPath = prevKeyPath;
1869
task.componentStack = preiousComponentStack;
1870
return;
@@ -1899,7 +1873,7 @@ function renderElement(
1873
if (enableScopeAPI) {
1874
const prevKeyPath = task.keyPath;
1875
task.keyPath = keyPath;
1902
- renderNodeDestructive(request, task, null, props.children, -1);
1876
+ renderNodeDestructive(request, task, props.children, -1);
1877
task.keyPath = prevKeyPath;
1878
return;
1879
}
@@ -1921,19 +1895,11 @@ function renderElement(
1895
if (typeof type === 'object' && type !== null) {
1896
switch (type.$$typeof) {
1897
case REACT_FORWARD_REF_TYPE: {
1924
- renderForwardRef(
1925
- request,
1926
- task,
1927
- keyPath,
1928
- prevThenableState,
1929
- type,
1930
- props,
1931
- ref,
1932
- );
1898
+ renderForwardRef(request, task, keyPath, type, props, ref);
1899
return;
1900
}
1901
case REACT_MEMO_TYPE: {
1936
- renderMemo(request, task, keyPath, prevThenableState, type, props, ref);
1902
+ renderMemo(request, task, keyPath, type, props, ref);
1903
return;
1904
}
1905
case REACT_PROVIDER_TYPE: {
@@ -1945,14 +1911,7 @@ function renderElement(
1911
return;
1912
}
1913
case REACT_LAZY_TYPE: {
1948
- renderLazyComponent(
1949
- request,
1950
- task,
1951
- keyPath,
1952
- prevThenableState,
1953
- type,
1954
- props,
1955
- );
1914
+ renderLazyComponent(request, task, keyPath, type, props);
1915
return;
1916
}
1917
}
@@ -2025,7 +1984,6 @@ function replayElement(
1984
request: Request,
1985
task: ReplayTask,
1986
keyPath: KeyNode,
2028
- prevThenableState: ThenableState | null,
1987
name: null | string,
1988
keyOrIndex: number | string,
1989
childIndex: number,
@@ -2060,15 +2018,7 @@ function replayElement(
2018
const currentNode = task.node;
2019
task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1};
2020
try {
2063
- renderElement(
2064
- request,
2065
- task,
2066
- keyPath,
2067
- prevThenableState,
2068
- type,
2069
- props,
2070
- ref,
2071
- );
2021
+ renderElement(request, task, keyPath, type, props, ref);
2022
if (
2023
task.replay.pendingTasks === 1 &&
2024
task.replay.nodes.length > 0
@@ -2184,9 +2134,6 @@ function validateIterable(iterable, iteratorFn: Function): void {
2134
function renderNodeDestructive(
2135
request: Request,
2136
task: Task,
2187
- // The thenable state reused from the previous attempt, if any. This is almost
2188
- // always null, except when called by retryTask.
2189
- prevThenableState: ThenableState | null,
2137
node: ReactNodeList,
2138
childIndex: number,
2139
): void {
@@ -2223,7 +2170,6 @@ function renderNodeDestructive(
2170
request,
2171
task,
2172
keyPath,
2226
- prevThenableState,
2173
name,
2174
keyOrIndex,
2175
childIndex,
@@ -2236,15 +2182,7 @@ function renderNodeDestructive(
2182
// prelude and skip it during the replay.
2183
} else {
2184
// We're doing a plain render.
2239
- renderElement(
2240
- request,
2241
- task,
2242
- keyPath,
2243
- prevThenableState,
2244
- type,
2245
- props,
2246
- ref,
2247
- );
2185
+ renderElement(request, task, keyPath, type, props, ref);
2186
}
2187
return;
2188
}
@@ -2266,7 +2204,7 @@ function renderNodeDestructive(
2204
task.componentStack = previousComponentStack;
2205
2206
// Now we render the resolved node
2269
- renderNodeDestructive(request, task, null, resolvedNode, childIndex);
2207
+ renderNodeDestructive(request, task, resolvedNode, childIndex);
2208
return;
2209
}
2210
}
@@ -2314,11 +2252,12 @@ function renderNodeDestructive(
2252
// e.g. Usable<Usable<Usable<T>>> should resolve to T
2253
const maybeUsable: Object = node;
2254
if (typeof maybeUsable.then === 'function') {
2255
+ // Clear any previous thenable state that was created by the unwrapping.
2256
+ task.thenableState = null;
2257
const thenable: Thenable<ReactNodeList> = (maybeUsable: any);
2258
return renderNodeDestructive(
2259
request,
2260
task,
2321
- null,
2261
unwrapThenable(thenable),
2262
childIndex,
2263
);
@@ -2332,7 +2271,6 @@ function renderNodeDestructive(
2271
return renderNodeDestructive(
2272
request,
2273
task,
2335
- null,
2274
readContext(context),
2275
childIndex,
2276
);
@@ -2827,7 +2765,7 @@ function renderNode(
2765
if (segment === null) {
2766
// Replay
2767
try {
2830
- return renderNodeDestructive(request, task, null, node, childIndex);
2768
+ return renderNodeDestructive(request, task, node, childIndex);
2769
} catch (thrownValue) {
2770
resetHooksState();
2771
@@ -2875,7 +2813,7 @@ function renderNode(
2813
const childrenLength = segment.children.length;
2814
const chunkLength = segment.chunks.length;
2815
try {
2878
- return renderNodeDestructive(request, task, null, node, childIndex);
2816
+ return renderNodeDestructive(request, task, node, childIndex);
2817
} catch (thrownValue) {
2818
resetHooksState();
2819
@@ -3456,19 +3394,7 @@ function retryRenderTask(
3394
// We call the destructive form that mutates this task. That way if something
3395
// suspends again, we can reuse the same task instead of spawning a new one.
3396
3459
- // Reset the task's thenable state before continuing, so that if a later
3460
- // component suspends we can reuse the same task object. If the same
3461
- // component suspends again, the thenable state will be restored.
3462
- const prevThenableState = task.thenableState;
3463
- task.thenableState = null;
3464
-
3465
- renderNodeDestructive(
3466
- request,
3467
- task,
3468
- prevThenableState,
3469
- task.node,
3470
- task.childIndex,
3471
- );
3397
+ renderNodeDestructive(request, task, task.node, task.childIndex);
3398
pushSegmentFinale(
3399
segment.chunks,
3400
request.renderState,
@@ -3559,19 +3485,7 @@ function retryReplayTask(request: Request, task: ReplayTask): void {
3485
// We call the destructive form that mutates this task. That way if something
3486
// suspends again, we can reuse the same task instead of spawning a new one.
3487
3562
- // Reset the task's thenable state before continuing, so that if a later
3563
- // component suspends we can reuse the same task object. If the same
3564
- // component suspends again, the thenable state will be restored.
3565
- const prevThenableState = task.thenableState;
3566
- task.thenableState = null;
3567
-
3568
- renderNodeDestructive(
3569
- request,
3570
- task,
3571
- prevThenableState,
3572
- task.node,
3573
- task.childIndex,
3574
- );
3488
+ renderNodeDestructive(request, task, task.node, task.childIndex);
3489
3490
if (task.replay.pendingTasks === 1 && task.replay.nodes.length > 0) {
3491
throw new Error(
packages/react-server/src/ReactFlightServer.js
+19
-40
@@ -507,7 +507,6 @@ function renderElement(
507
key: null | React$Key,
508
ref: mixed,
509
props: any,
510
- prevThenableState: ThenableState | null,
510
): ReactJSONValue {
511
if (ref !== null && ref !== undefined) {
512
// When the ref moves to the regular props object this will implicitly
@@ -529,6 +528,13 @@ function renderElement(
528
return [REACT_ELEMENT_TYPE, type, key, props];
529
}
530
// This is a server-side component.
531
+
532
+ // Reset the task's thenable state before continuing, so that if a later
533
+ // component suspends we can reuse the same task object. If the same
534
+ // component suspends again, the thenable state will be restored.
535
+ const prevThenableState = task.thenableState;
536
+ task.thenableState = null;
537
+
538
prepareToUseHooksForComponent(prevThenableState);
539
let result = type(props);
540
if (
@@ -546,7 +552,7 @@ function renderElement(
552
// the thenable here.
553
result = createLazyWrapperAroundWakeable(result);
554
}
549
- return renderModelDestructive(request, task, emptyRoot, '', result, null);
555
+ return renderModelDestructive(request, task, emptyRoot, '', result);
556
} else if (typeof type === 'string') {
557
// This is a host element. E.g. HTML.
558
return [REACT_ELEMENT_TYPE, type, key, props];
@@ -562,7 +568,6 @@ function renderElement(
568
emptyRoot,
569
'',
570
props.children,
565
- null,
571
);
572
}
573
// This might be a built-in React component. We'll let the client decide.
@@ -578,39 +583,23 @@ function renderElement(
583
const payload = type._payload;
584
const init = type._init;
585
const wrappedType = init(payload);
581
- return renderElement(
582
- request,
583
- task,
584
- wrappedType,
585
- key,
586
- ref,
587
- props,
588
- prevThenableState,
589
- );
586
+ return renderElement(request, task, wrappedType, key, ref, props);
587
}
588
case REACT_FORWARD_REF_TYPE: {
589
const render = type.render;
590
+
591
+ // Reset the task's thenable state before continuing, so that if a later
592
+ // component suspends we can reuse the same task object. If the same
593
+ // component suspends again, the thenable state will be restored.
594
+ const prevThenableState = task.thenableState;
595
+ task.thenableState = null;
596
+
597
prepareToUseHooksForComponent(prevThenableState);
598
const result = render(props, undefined);
595
- return renderModelDestructive(
596
- request,
597
- task,
598
- emptyRoot,
599
- '',
600
- result,
601
- null,
602
- );
599
+ return renderModelDestructive(request, task, emptyRoot, '', result);
600
}
601
case REACT_MEMO_TYPE: {
605
- return renderElement(
606
- request,
607
- task,
608
- type.type,
609
- key,
610
- ref,
611
- props,
612
- prevThenableState,
613
- );
602
+ return renderElement(request, task, type.type, key, ref, props);
603
}
604
case REACT_PROVIDER_TYPE: {
605
if (enableServerContext) {
@@ -1000,7 +989,7 @@ function renderModel(
989
value: ReactClientValue,
990
): ReactJSONValue {
991
try {
1003
- return renderModelDestructive(request, task, parent, key, value, null);
992
+ return renderModelDestructive(request, task, parent, key, value);
993
} catch (thrownValue) {
994
const x =
995
thrownValue === SuspenseException
@@ -1076,7 +1065,6 @@ function renderModelDestructive(
1065
| $ReadOnlyArray<ReactClientValue>,
1066
parentPropertyName: string,
1067
value: ReactClientValue,
1079
- prevThenableState: ThenableState | null,
1068
): ReactJSONValue {
1069
// Set the currently rendering model
1070
task.model = value;
@@ -1130,7 +1118,6 @@ function renderModelDestructive(
1118
element.key,
1119
element.ref,
1120
element.props,
1133
- prevThenableState,
1121
);
1122
}
1123
case REACT_LAZY_TYPE: {
@@ -1143,7 +1130,6 @@ function renderModelDestructive(
1130
emptyRoot,
1131
'',
1132
resolvedModel,
1146
- null,
1133
);
1134
}
1135
}
@@ -1598,12 +1584,6 @@ function retryTask(request: Request, task: Task): void {
1584
1585
switchContext(task.context);
1586
try {
1601
- // Reset the task's thenable state before continuing, so that if a later
1602
- // component suspends we can reuse the same task object. If the same
1603
- // component suspends again, the thenable state will be restored.
1604
- const prevThenableState = task.thenableState;
1605
- task.thenableState = null;
1606
-
1587
// Track the root so we know that we have to emit this object even though it
1588
// already has an ID. This is needed because we might see this object twice
1589
// in the same toJSON if it is cyclic.
@@ -1617,7 +1597,6 @@ function retryTask(request: Request, task: Task): void {
1597
emptyRoot,
1598
'',
1599
task.model,
1620
- prevThenableState,
1600
);
1601
1602
// Track the root again for the resolved object.