Skip to content

Commit 99781d6

Browse files
authored
[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.
1 parent 459a2c4 commit 99781d6

File tree

2 files changed

+87
-48
lines changed

2 files changed

+87
-48
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2262,6 +2262,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
22622262
<ComponentB />
22632263
</Suspense>
22642264
<Suspense fallback="Loading C">C</Suspense>
2265+
<Suspense fallback="Loading D">D</Suspense>
22652266
</SuspenseList>
22662267
</div>
22672268
);
@@ -2282,14 +2283,16 @@ describe('ReactDOMFizzStaticBrowser', () => {
22822283
});
22832284

22842285
const prerendered = await pendingResult;
2286+
22852287
const postponedState = JSON.stringify(prerendered.postponed);
22862288

22872289
await readIntoContainer(prerendered.prelude);
22882290
expect(getVisibleChildren(container)).toEqual(
22892291
<div>
22902292
{'Loading A'}
22912293
{'Loading B'}
2292-
{'C' /* TODO: This should not be resolved. */}
2294+
{'Loading C'}
2295+
{'Loading D'}
22932296
</div>,
22942297
);
22952298

@@ -2309,6 +2312,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
23092312
{'A'}
23102313
{'B'}
23112314
{'C'}
2315+
{'D'}
23122316
</div>,
23132317
);
23142318
});

packages/react-server/src/ReactFizzServer.js

Lines changed: 82 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1725,6 +1725,26 @@ function unblockSuspenseListRow(
17251725
}
17261726
}
17271727

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+
17281748
function tryToResolveTogetherRow(
17291749
request: Request,
17301750
togetherRow: SuspenseListRow,
@@ -3774,6 +3794,49 @@ function renderChildrenArray(
37743794
}
37753795
}
37763796

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+
37773840
function trackPostpone(
37783841
request: Request,
37793842
trackedPostpones: PostponedHoles,
@@ -3796,22 +3859,12 @@ function trackPostpone(
37963859
}
37973860

37983861
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) {
38153868
// Assign ID
38163869
if (segment.id === -1) {
38173870
if (segment.parentFlushed) {
@@ -3823,39 +3876,10 @@ function trackPostpone(
38233876
}
38243877
}
38253878
// 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;
38363880
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.
38583881
}
3882+
// Otherwise, fall through to add the child node.
38593883
}
38603884

38613885
// We know that this will leave a hole so we might as well assign an ID now.
@@ -4941,7 +4965,18 @@ function finishedTask(
49414965
} else if (boundary.status === POSTPONED) {
49424966
const boundaryRow = boundary.row;
49434967
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+
}
49444977
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.
49454980
finishSuspenseListRow(request, boundaryRow);
49464981
}
49474982
}

0 commit comments

Comments
 (0)