@samitouri / QOS-React-1 / commits / 99781d605b

[Fizz] Track boundaries in future rows as postponed (#33329)

Follow up to #33321. We can mark boundaries that were blocked in the prerender as postponed but without anything to replayed inside them. That way they're not emitted in the prerender but is unblocked when replayed. Technically this does some unnecessary replaying of the path to the otherwise already completed boundary but it simplifies our model by just marking the boundary as needing replaying.

Sebastian Markbåge committed May 22, 2025 at 10:20 UTC 99781d605b6a6fd332bba654ac0c328e41df888c
2 files changed +87 -48
packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
+5 -1
@@ -2262,6 +2262,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
2262 <ComponentB />
2263 </Suspense>
2264 <Suspense fallback="Loading C">C</Suspense>
2265 + <Suspense fallback="Loading D">D</Suspense>
2266 </SuspenseList>
2267 </div>
2268 );
@@ -2282,6 +2283,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
2283 });
2284
2285 const prerendered = await pendingResult;
2286 +
2287 const postponedState = JSON.stringify(prerendered.postponed);
2288
2289 await readIntoContainer(prerendered.prelude);
@@ -2289,7 +2291,8 @@ describe('ReactDOMFizzStaticBrowser', () => {
2291 <div>
2292 {'Loading A'}
2293 {'Loading B'}
2292 - {'C' /* TODO: This should not be resolved. */}
2294 + {'Loading C'}
2295 + {'Loading D'}
2296 </div>,
2297 );
2298
@@ -2309,6 +2312,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
2312 {'A'}
2313 {'B'}
2314 {'C'}
2315 + {'D'}
2316 </div>,
2317 );
2318 });
packages/react-server/src/ReactFizzServer.js
+82 -47
@@ -1725,6 +1725,26 @@ function unblockSuspenseListRow(
1725 }
1726 }
1727
1728 +function trackPostponedSuspenseListRow(
1729 + request: Request,
1730 + trackedPostpones: PostponedHoles,
1731 + postponedRow: null | SuspenseListRow,
1732 +): void {
1733 + // TODO: Because we unconditionally call this, it will be called by finishedTask
1734 + // and so ends up recursive which can lead to stack overflow for very long lists.
1735 + if (postponedRow !== null) {
1736 + const postponedBoundaries = postponedRow.boundaries;
1737 + if (postponedBoundaries !== null) {
1738 + postponedRow.boundaries = null;
1739 + for (let i = 0; i < postponedBoundaries.length; i++) {
1740 + const postponedBoundary = postponedBoundaries[i];
1741 + trackPostponedBoundary(request, trackedPostpones, postponedBoundary);
1742 + finishedTask(request, postponedBoundary, null, null);
1743 + }
1744 + }
1745 + }
1746 +}
1747 +
1748 function tryToResolveTogetherRow(
1749 request: Request,
1750 togetherRow: SuspenseListRow,
@@ -3774,6 +3794,49 @@ function renderChildrenArray(
3794 }
3795 }
3796
3797 +function trackPostponedBoundary(
3798 + request: Request,
3799 + trackedPostpones: PostponedHoles,
3800 + boundary: SuspenseBoundary,
3801 +): ReplaySuspenseBoundary {
3802 + boundary.status = POSTPONED;
3803 + // We need to eagerly assign it an ID because we'll need to refer to
3804 + // it before flushing and we know that we can't inline it.
3805 + boundary.rootSegmentID = request.nextSegmentId++;
3806 +
3807 + const boundaryKeyPath = boundary.trackedContentKeyPath;
3808 + if (boundaryKeyPath === null) {
3809 + throw new Error(
3810 + 'It should not be possible to postpone at the root. This is a bug in React.',
3811 + );
3812 + }
3813 +
3814 + const fallbackReplayNode = boundary.trackedFallbackNode;
3815 +
3816 + const children: Array<ReplayNode> = [];
3817 + const boundaryNode: void | ReplayNode =
3818 + trackedPostpones.workingMap.get(boundaryKeyPath);
3819 + if (boundaryNode === undefined) {
3820 + const suspenseBoundary: ReplaySuspenseBoundary = [
3821 + boundaryKeyPath[1],
3822 + boundaryKeyPath[2],
3823 + children,
3824 + null,
3825 + fallbackReplayNode,
3826 + boundary.rootSegmentID,
3827 + ];
3828 + trackedPostpones.workingMap.set(boundaryKeyPath, suspenseBoundary);
3829 + addToReplayParent(suspenseBoundary, boundaryKeyPath[0], trackedPostpones);
3830 + return suspenseBoundary;
3831 + } else {
3832 + // Upgrade to ReplaySuspenseBoundary.
3833 + const suspenseBoundary: ReplaySuspenseBoundary = (boundaryNode: any);
3834 + suspenseBoundary[4] = fallbackReplayNode;
3835 + suspenseBoundary[5] = boundary.rootSegmentID;
3836 + return suspenseBoundary;
3837 + }
3838 +}
3839 +
3840 function trackPostpone(
3841 request: Request,
3842 trackedPostpones: PostponedHoles,
@@ -3796,22 +3859,12 @@ function trackPostpone(
3859 }
3860
3861 if (boundary !== null && boundary.status === PENDING) {
3799 - boundary.status = POSTPONED;
3800 - // We need to eagerly assign it an ID because we'll need to refer to
3801 - // it before flushing and we know that we can't inline it.
3802 - boundary.rootSegmentID = request.nextSegmentId++;
3803 -
3804 - const boundaryKeyPath = boundary.trackedContentKeyPath;
3805 - if (boundaryKeyPath === null) {
3806 - throw new Error(
3807 - 'It should not be possible to postpone at the root. This is a bug in React.',
3808 - );
3809 - }
3810 -
3811 - const fallbackReplayNode = boundary.trackedFallbackNode;
3812 -
3813 - const children: Array<ReplayNode> = [];
3814 - if (boundaryKeyPath === keyPath && task.childIndex === -1) {
3862 + const boundaryNode = trackPostponedBoundary(
3863 + request,
3864 + trackedPostpones,
3865 + boundary,
3866 + );
3867 + if (boundary.trackedContentKeyPath === keyPath && task.childIndex === -1) {
3868 // Assign ID
3869 if (segment.id === -1) {
3870 if (segment.parentFlushed) {
@@ -3823,39 +3876,10 @@ function trackPostpone(
3876 }
3877 }
3878 // We postponed directly inside the Suspense boundary so we mark this for resuming.
3826 - const boundaryNode: ReplaySuspenseBoundary = [
3827 - boundaryKeyPath[1],
3828 - boundaryKeyPath[2],
3829 - children,
3830 - segment.id,
3831 - fallbackReplayNode,
3832 - boundary.rootSegmentID,
3833 - ];
3834 - trackedPostpones.workingMap.set(boundaryKeyPath, boundaryNode);
3835 - addToReplayParent(boundaryNode, boundaryKeyPath[0], trackedPostpones);
3879 + boundaryNode[3] = segment.id;
3880 return;
3837 - } else {
3838 - let boundaryNode: void | ReplayNode =
3839 - trackedPostpones.workingMap.get(boundaryKeyPath);
3840 - if (boundaryNode === undefined) {
3841 - boundaryNode = [
3842 - boundaryKeyPath[1],
3843 - boundaryKeyPath[2],
3844 - children,
3845 - null,
3846 - fallbackReplayNode,
3847 - boundary.rootSegmentID,
3848 - ];
3849 - trackedPostpones.workingMap.set(boundaryKeyPath, boundaryNode);
3850 - addToReplayParent(boundaryNode, boundaryKeyPath[0], trackedPostpones);
3851 - } else {
3852 - // Upgrade to ReplaySuspenseBoundary.
3853 - const suspenseBoundary: ReplaySuspenseBoundary = (boundaryNode: any);
3854 - suspenseBoundary[4] = fallbackReplayNode;
3855 - suspenseBoundary[5] = boundary.rootSegmentID;
3856 - }
3857 - // Fall through to add the child node.
3881 }
3882 + // Otherwise, fall through to add the child node.
3883 }
3884
3885 // We know that this will leave a hole so we might as well assign an ID now.
@@ -4941,7 +4965,18 @@ function finishedTask(
4965 } else if (boundary.status === POSTPONED) {
4966 const boundaryRow = boundary.row;
4967 if (boundaryRow !== null) {
4968 + if (request.trackedPostpones !== null) {
4969 + // If this boundary is postponed, then we need to also postpone any blocked boundaries
4970 + // in the next row.
4971 + trackPostponedSuspenseListRow(
4972 + request,
4973 + request.trackedPostpones,
4974 + boundaryRow.next,
4975 + );
4976 + }
4977 if (--boundaryRow.pendingTasks === 0) {
4978 + // This is really unnecessary since we've already postponed the boundaries but
4979 + // for pairity with other track+finish paths. We might end up using the hoisting.
4980 finishSuspenseListRow(request, boundaryRow);
4981 }
4982 }