@samitouri / QOS-React-1 / commits / 382190c595

[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.