@samitouri / QOS-React-2 / commits / 7a3cb8f9cf

Track the key path difference between right before the first array (#27360)

There's a subtle difference if you suspend before the first array or after. In Fiber, we don't deal with this because we just suspend the parent and replay it if lazy() or Usable are used in its child slots. In Fizz we try to optimize this a bit more and enable resuming inside the component. Semantically, it's different if you suspend/postpone before the first child array or inside that child array. Because when you resume the inner result might be another array and either that's part of the parent path or part of the inner slot. There might be more clever way of structuring this but I just use -1 to indicate that we're not yet inside the array and is in the root child position. If that renders an element, then that's just the same as the 0 slot. We need to also encode this in the resuming. I called that resuming the element or resuming the slot.

Sebastian Markbåge committed Sep 12, 2023 at 12:30 UTC 7a3cb8f9cf43591afc74722ece9e3216ccc98128
2 files changed +50 -71
packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
+1 -1
@@ -461,7 +461,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
461 if (prerendering) {
462 React.unstable_postpone();
463 }
464 - return 'Hello';
464 + return ['Hello', 'World'];
465 }
466
467 function App() {
packages/react-server/src/ReactFizzServer.js
+49 -70
@@ -166,7 +166,8 @@ type KeyNode = [
166
167 const REPLAY_NODE = 0;
168 const REPLAY_SUSPENSE_BOUNDARY = 1;
169 -const RESUME_SEGMENT = 2;
169 +const RESUME_ELEMENT = 2;
170 +const RESUME_SLOT = 3;
171
172 type ResumableParentNode =
173 | [
@@ -185,7 +186,13 @@ type ResumableParentNode =
186 type ResumableNode =
187 | ResumableParentNode
188 | [
188 - 2, // RESUME_SEGMENT
189 + 2, // RESUME_ELEMENT
190 + string | null /* name */,
191 + string | number /* key */,
192 + number /* segment id */,
193 + ]
194 + | [
195 + 3, // RESUME_SLOT
196 number /* index */,
197 number /* segment id */,
198 ];
@@ -784,7 +791,7 @@ function renderSuspenseBoundary(
791 }
792 try {
793 // We use the safe form because we don't handle suspending here. Only error handling.
787 - renderNode(request, task, content, 0);
794 + renderNode(request, task, content, -1);
795 pushSegmentFinale(
796 contentRootSegment.chunks,
797 request.renderState,
@@ -873,7 +880,7 @@ function renderBackupSuspenseBoundary(
880 const segment = task.blockedSegment;
881
882 pushStartCompletedSuspenseBoundary(segment.chunks);
876 - renderNode(request, task, content, 0);
883 + renderNode(request, task, content, -1);
884 pushEndCompletedSuspenseBoundary(segment.chunks);
885
886 popComponentStackInDEV(task);
@@ -903,7 +910,7 @@ function renderHostElement(
910
911 // We use the non-destructive form because if something suspends, we still
912 // need to pop back up and finish this subtree of HTML.
906 - renderNode(request, task, children, 0);
913 + renderNode(request, task, children, -1);
914
915 // We expect that errors will fatal the whole task and that we don't need
916 // the correct context. Therefore this is not in a finally.
@@ -970,13 +977,13 @@ function finishClassComponent(
977 childContextTypes,
978 );
979 task.legacyContext = mergedContext;
973 - renderNodeDestructive(request, task, null, nextChildren, 0);
980 + renderNodeDestructive(request, task, null, nextChildren, -1);
981 task.legacyContext = previousContext;
982 return;
983 }
984 }
985
979 - renderNodeDestructive(request, task, null, nextChildren, 0);
986 + renderNodeDestructive(request, task, null, nextChildren, -1);
987 }
988
989 function renderClassComponent(
@@ -1170,7 +1177,7 @@ function finishFunctionComponent(
1177 // Modify the id context. Because we'll need to reset this if something
1178 // suspends or errors, we'll use the non-destructive render path.
1179 task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index);
1173 - renderNode(request, task, children, 0);
1180 + renderNode(request, task, children, -1);
1181 // Like the other contexts, this does not need to be in a finally block
1182 // because renderNode takes care of unwinding the stack.
1183 task.treeContext = prevTreeContext;
@@ -1178,12 +1185,12 @@ function finishFunctionComponent(
1185 // If there were formState hooks, we must use the non-destructive path
1186 // because this component is not a pure indirection; we emitted markers
1187 // to the stream.
1181 - renderNode(request, task, children, 0);
1188 + renderNode(request, task, children, -1);
1189 } else {
1190 // We're now successfully past this task, and we haven't modified the
1191 // context stack. We don't have to pop back to the previous task every
1192 // again, so we can use the destructive recursive form.
1186 - renderNodeDestructive(request, task, null, children, 0);
1193 + renderNodeDestructive(request, task, null, children, -1);
1194 }
1195 }
1196
@@ -1353,7 +1360,7 @@ function renderContextConsumer(
1360 const newValue = readContext(context);
1361 const newChildren = render(newValue);
1362
1356 - renderNodeDestructive(request, task, null, newChildren, 0);
1363 + renderNodeDestructive(request, task, null, newChildren, -1);
1364 }
1365
1366 function renderContextProvider(
@@ -1370,7 +1377,7 @@ function renderContextProvider(
1377 prevSnapshot = task.context;
1378 }
1379 task.context = pushProvider(context, value);
1373 - renderNodeDestructive(request, task, null, children, 0);
1380 + renderNodeDestructive(request, task, null, children, -1);
1381 task.context = popProvider(context);
1382 if (__DEV__) {
1383 if (prevSnapshot !== task.context) {
@@ -1413,7 +1420,7 @@ function renderOffscreen(request: Request, task: Task, props: Object): void {
1420 } else {
1421 // A visible Offscreen boundary is treated exactly like a fragment: a
1422 // pure indirection.
1416 - renderNodeDestructive(request, task, null, props.children, 0);
1423 + renderNodeDestructive(request, task, null, props.children, -1);
1424 }
1425 }
1426
@@ -1460,7 +1467,7 @@ function renderElement(
1467 case REACT_STRICT_MODE_TYPE:
1468 case REACT_PROFILER_TYPE:
1469 case REACT_FRAGMENT_TYPE: {
1463 - renderNodeDestructive(request, task, null, props.children, 0);
1470 + renderNodeDestructive(request, task, null, props.children, -1);
1471 return;
1472 }
1473 case REACT_OFFSCREEN_TYPE: {
@@ -1470,13 +1477,13 @@ function renderElement(
1477 case REACT_SUSPENSE_LIST_TYPE: {
1478 pushBuiltInComponentStackInDEV(task, 'SuspenseList');
1479 // TODO: SuspenseList should control the boundaries.
1473 - renderNodeDestructive(request, task, null, props.children, 0);
1480 + renderNodeDestructive(request, task, null, props.children, -1);
1481 popComponentStackInDEV(task);
1482 return;
1483 }
1484 case REACT_SCOPE_TYPE: {
1485 if (enableScopeAPI) {
1479 - renderNodeDestructive(request, task, null, props.children, 0);
1486 + renderNodeDestructive(request, task, null, props.children, -1);
1487 return;
1488 }
1489 throw new Error('ReactDOMServer does not yet support scope components.');
@@ -1645,7 +1652,11 @@ function renderNodeDestructiveImpl(
1652 const ref = element.ref;
1653 const name = getComponentNameFromType(type);
1654 const prevKeyPath = task.keyPath;
1648 - task.keyPath = [task.keyPath, name, key == null ? childIndex : key];
1655 + task.keyPath = [
1656 + task.keyPath,
1657 + name,
1658 + key == null ? (childIndex === -1 ? 0 : childIndex) : key,
1659 + ];
1660 renderElement(request, task, prevThenableState, type, props, ref);
1661 task.keyPath = prevKeyPath;
1662 return;
@@ -1805,47 +1816,15 @@ function renderChildrenArray(
1816 children: Array<any>,
1817 childIndex: number,
1818 ) {
1819 + const prevKeyPath = task.keyPath;
1820 + if (childIndex !== -1) {
1821 + task.keyPath = [task.keyPath, '', childIndex];
1822 + }
1823 const prevTreeContext = task.treeContext;
1824 const totalChildren = children.length;
1825 for (let i = 0; i < totalChildren; i++) {
1826 const node = children[i];
1827 task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i);
1813 -
1814 - // Nested arrays behave like a "fragment node" which is keyed.
1815 - // Therefore we need to add the current index as a parent key.
1816 - // We first check if the nested nodes are arrays or iterables.
1817 -
1818 - if (isArray(node)) {
1819 - const prevKeyPath = task.keyPath;
1820 - task.keyPath = [task.keyPath, '', childIndex];
1821 - renderChildrenArray(request, task, node, i);
1822 - task.keyPath = prevKeyPath;
1823 - continue;
1824 - }
1825 -
1826 - const iteratorFn = getIteratorFn(node);
1827 - if (iteratorFn) {
1828 - if (__DEV__) {
1829 - validateIterable(node, iteratorFn);
1830 - }
1831 - const iterator = iteratorFn.call(node);
1832 - if (iterator) {
1833 - let step = iterator.next();
1834 - if (!step.done) {
1835 - const prevKeyPath = task.keyPath;
1836 - task.keyPath = [task.keyPath, '', childIndex];
1837 - const nestedChildren = [];
1838 - do {
1839 - nestedChildren.push(step.value);
1840 - step = iterator.next();
1841 - } while (!step.done);
1842 - renderChildrenArray(request, task, nestedChildren, i);
1843 - task.keyPath = prevKeyPath;
1844 - }
1845 - continue;
1846 - }
1847 - }
1848 -
1828 // We need to use the non-destructive form so that we can safely pop back
1829 // up and render the sibling if something suspends.
1830 renderNode(request, task, node, i);
@@ -1853,13 +1832,13 @@ function renderChildrenArray(
1832 // Because this context is always set right before rendering every child, we
1833 // only need to reset it to the previous value at the very end.
1834 task.treeContext = prevTreeContext;
1835 + task.keyPath = prevKeyPath;
1836 }
1837
1838 function trackPostpone(
1839 request: Request,
1840 trackedPostpones: PostponedHoles,
1841 task: Task,
1862 - childIndex: number,
1842 segment: Segment,
1843 ): void {
1844 segment.status = POSTPONED;
@@ -1901,8 +1880,20 @@ function trackPostpone(
1880 );
1881 }
1882
1904 - const segmentNode: ResumableNode = [RESUME_SEGMENT, childIndex, segment.id];
1905 - addToResumableParent(segmentNode, keyPath, trackedPostpones);
1883 + if (task.childIndex === -1) {
1884 + // Resume at the position before the first array
1885 + const resumableElement = [
1886 + RESUME_ELEMENT,
1887 + keyPath[1],
1888 + keyPath[2],
1889 + segment.id,
1890 + ];
1891 + addToResumableParent(resumableElement, keyPath[0], trackedPostpones);
1892 + } else {
1893 + // Resume at the slot within the array
1894 + const resumableNode = [RESUME_SLOT, task.childIndex, segment.id];
1895 + addToResumableParent(resumableNode, keyPath, trackedPostpones);
1896 + }
1897 }
1898
1899 function injectPostponedHole(
@@ -2060,13 +2051,7 @@ function renderNode(
2051 task,
2052 postponeInstance.message,
2053 );
2063 - trackPostpone(
2064 - request,
2065 - trackedPostpones,
2066 - task,
2067 - childIndex,
2068 - postponedSegment,
2069 - );
2054 + trackPostpone(request, trackedPostpones, task, postponedSegment);
2055
2056 // Restore the context. We assume that this will be restored by the inner
2057 // functions in case nothing throws so we don't use "finally" here.
@@ -2414,13 +2399,7 @@ function retryTask(request: Request, task: Task): void {
2399 task.abortSet.delete(task);
2400 const postponeInstance: Postpone = (x: any);
2401 logPostpone(request, postponeInstance.message);
2417 - trackPostpone(
2418 - request,
2419 - trackedPostpones,
2420 - task,
2421 - task.childIndex,
2422 - segment,
2423 - );
2402 + trackPostpone(request, trackedPostpones, task, segment);
2403 finishedTask(request, task.blockedBoundary, segment);
2404 return;
2405 }