From c91892ec3c180a8f85a030aea931b58e326a55a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 8 Mar 2022 23:12:50 -0500 Subject: [PATCH] [Fizz] Don't flush empty segments (#24054) Before this change, we would sometimes write segments without any content in them. For example for a Suspense boundary that immediately suspends we might emit something like: Where the outer div is just a temporary wrapper and the inner one is a placeholder for something to be added later. This serves no purpose. We should ideally have a heuristic that holds back segments based on byte size and time. However, this is a straight forward clear win for now. --- .../src/__tests__/ReactDOMFizzServer-test.js | 4 +++ packages/react-server/src/ReactFizzServer.js | 29 +++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 06e83ba128be3..3ced96af29564 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -495,6 +495,10 @@ describe('ReactDOMFizzServer', () => { pipe(writable); }); expect(getVisibleChildren(container)).toEqual(
Loading...
); + // Because there is no content inside the Suspense boundary that could've + // been written, we expect to not see any additional partial data flushed + // yet. + expect(container.firstChild.nextSibling).toBe(null); await act(async () => { resolveElement({default: }); }); diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 7a238391493a3..c09d21cb55cf5 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -488,7 +488,7 @@ function renderSuspenseBoundary( // We use the safe form because we don't handle suspending here. Only error handling. renderNode(request, task, content); contentRootSegment.status = COMPLETED; - newBoundary.completedSegments.push(contentRootSegment); + queueCompletedSegment(newBoundary, contentRootSegment); if (newBoundary.pendingTasks === 0) { // This must have been the last segment we were waiting on. This boundary is now complete. // Therefore we won't need the fallback. We early return so that we don't have to create @@ -1430,6 +1430,29 @@ function abortTask(task: Task): void { } } +function queueCompletedSegment( + boundary: SuspenseBoundary, + segment: Segment, +): void { + if ( + segment.chunks.length === 0 && + segment.children.length === 1 && + segment.children[0].boundary === null + ) { + // This is an empty segment. There's nothing to write, so we can instead transfer the ID + // to the child. That way any existing references point to the child. + const childSegment = segment.children[0]; + childSegment.id = segment.id; + childSegment.parentFlushed = true; + if (childSegment.status === COMPLETED) { + queueCompletedSegment(boundary, childSegment); + } + } else { + const completedSegments = boundary.completedSegments; + completedSegments.push(segment); + } +} + function finishedTask( request: Request, boundary: Root | SuspenseBoundary, @@ -1463,7 +1486,7 @@ function finishedTask( // If it is a segment that was aborted, we'll write other content instead so we don't need // to emit it. if (segment.status === COMPLETED) { - boundary.completedSegments.push(segment); + queueCompletedSegment(boundary, segment); } } if (boundary.parentFlushed) { @@ -1482,8 +1505,8 @@ function finishedTask( // If it is a segment that was aborted, we'll write other content instead so we don't need // to emit it. if (segment.status === COMPLETED) { + queueCompletedSegment(boundary, segment); const completedSegments = boundary.completedSegments; - completedSegments.push(segment); if (completedSegments.length === 1) { // This is the first time since we last flushed that we completed anything. // We can schedule this boundary to emit its partially completed segments early