diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js
index f3a1b37103..5dcc3c9c3b 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js
@@ -335,4 +335,67 @@ describe('ReactDOMFizzStatic', () => {
});
expect(getVisibleChildren(container)).toEqual(undefined);
});
+
+ // @gate experimental
+ it('will prerender Suspense fallbacks before children', async () => {
+ const values = [];
+ function Indirection({children}) {
+ values.push(children);
+ return children;
+ }
+
+ function App() {
+ return (
+
+
+ outer loading...
+
+ }>
+
+ first inner loading...
+
+ }>
+
+ hello world
+
+
+
+ second inner loading...
+
+ }>
+
+ goodbye world
+
+
+
+
+ );
+ }
+
+ const result = await ReactDOMFizzStatic.prerenderToNodeStream();
+
+ expect(values).toEqual([
+ 'outer loading...',
+ 'first inner loading...',
+ 'second inner loading...',
+ 'hello world',
+ 'goodbye world',
+ ]);
+
+ await act(async () => {
+ result.prelude.pipe(writable);
+ });
+ expect(getVisibleChildren(container)).toEqual(
+
+
hello world
+
goodbye world
+
,
+ );
+ });
});
diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js
index e0adcfece0..ade755bdff 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js
@@ -111,9 +111,7 @@ describe('ReactDOMFizzStaticNode', () => {
const result = await resultPromise;
const prelude = await readContent(result.prelude);
- expect(prelude).toMatchInlineSnapshot(
- `"Done
"`,
- );
+ expect(prelude).toMatchInlineSnapshot(`"Done
"`);
});
// @gate experimental
diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js
index a1ca5dbaa2..85fa3c65d4 100644
--- a/packages/react-server/src/ReactFizzServer.js
+++ b/packages/react-server/src/ReactFizzServer.js
@@ -1138,83 +1138,13 @@ function renderSuspenseBoundary(
// no parent segment so there's nothing to wait on.
contentRootSegment.parentFlushed = true;
- // Currently this is running synchronously. We could instead schedule this to pingedTasks.
- // I suspect that there might be some efficiency benefits from not creating the suspended task
- // and instead just using the stack if possible.
- // TODO: Call this directly instead of messing with saving and restoring contexts.
+ if (request.trackedPostpones !== null) {
+ // This is a prerender. In this mode we want to render the fallback synchronously and schedule
+ // the content to render later. This is the opposite of what we do during a normal render
+ // where we try to skip rendering the fallback if the content itself can render synchronously
+ const trackedPostpones = request.trackedPostpones;
- // We can reuse the current context and task to render the content immediately without
- // context switching. We just need to temporarily switch which boundary and which segment
- // we're writing to. If something suspends, it'll spawn new suspended task with that context.
- task.blockedBoundary = newBoundary;
- task.hoistableState = newBoundary.contentState;
- task.blockedSegment = contentRootSegment;
- task.keyPath = keyPath;
-
- try {
- // We use the safe form because we don't handle suspending here. Only error handling.
- renderNode(request, task, content, -1);
- pushSegmentFinale(
- contentRootSegment.chunks,
- request.renderState,
- contentRootSegment.lastPushedText,
- contentRootSegment.textEmbedded,
- );
- contentRootSegment.status = COMPLETED;
- queueCompletedSegment(newBoundary, contentRootSegment);
- if (newBoundary.pendingTasks === 0 && newBoundary.status === PENDING) {
- // 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
- // the fallback.
- newBoundary.status = COMPLETED;
- return;
- }
- } catch (error: mixed) {
- contentRootSegment.status = ERRORED;
- newBoundary.status = CLIENT_RENDERED;
- const thrownInfo = getThrownInfo(task.componentStack);
- let errorDigest;
- if (
- enablePostpone &&
- typeof error === 'object' &&
- error !== null &&
- error.$$typeof === REACT_POSTPONE_TYPE
- ) {
- const postponeInstance: Postpone = (error: any);
- logPostpone(
- request,
- postponeInstance.message,
- thrownInfo,
- __DEV__ && enableOwnerStacks ? task.debugTask : null,
- );
- // TODO: Figure out a better signal than a magic digest value.
- errorDigest = 'POSTPONE';
- } else {
- errorDigest = logRecoverableError(
- request,
- error,
- thrownInfo,
- __DEV__ && enableOwnerStacks ? task.debugTask : null,
- );
- }
- encodeErrorForBoundary(newBoundary, errorDigest, error, thrownInfo, false);
-
- untrackBoundary(request, newBoundary);
-
- // We don't need to decrement any task numbers because we didn't spawn any new task.
- // We don't need to schedule any task because we know the parent has written yet.
- // We do need to fallthrough to create the fallback though.
- } finally {
- task.blockedBoundary = parentBoundary;
- task.hoistableState = parentHoistableState;
- task.blockedSegment = parentSegment;
- task.keyPath = prevKeyPath;
- }
-
- const fallbackKeyPath = [keyPath[0], 'Suspense Fallback', keyPath[2]];
- const trackedPostpones = request.trackedPostpones;
- if (trackedPostpones !== null) {
- // We create a detached replay node to track any postpones inside the fallback.
+ const fallbackKeyPath = [keyPath[0], 'Suspense Fallback', keyPath[2]];
const fallbackReplayNode: ReplayNode = [
fallbackKeyPath[1],
fallbackKeyPath[2],
@@ -1222,41 +1152,158 @@ function renderSuspenseBoundary(
null,
];
trackedPostpones.workingMap.set(fallbackKeyPath, fallbackReplayNode);
- if (newBoundary.status === POSTPONED) {
- // This must exist now.
- const boundaryReplayNode: ReplaySuspenseBoundary =
- (trackedPostpones.workingMap.get(keyPath): any);
- boundaryReplayNode[4] = fallbackReplayNode;
- } else {
- // We might not inject it into the postponed tree, unless the content actually
- // postpones too. We need to keep track of it until that happpens.
- newBoundary.trackedFallbackNode = fallbackReplayNode;
+ // We are rendering the fallback before the boundary content so we keep track of
+ // the fallback replay node until we determine if the primary content suspends
+ newBoundary.trackedFallbackNode = fallbackReplayNode;
+
+ task.blockedSegment = boundarySegment;
+ task.keyPath = fallbackKeyPath;
+ try {
+ renderNode(request, task, fallback, -1);
+ pushSegmentFinale(
+ boundarySegment.chunks,
+ request.renderState,
+ boundarySegment.lastPushedText,
+ boundarySegment.textEmbedded,
+ );
+ boundarySegment.status = COMPLETED;
+ } finally {
+ task.blockedSegment = parentSegment;
+ task.keyPath = prevKeyPath;
}
+
+ // We create a suspended task for the primary content because we want to allow
+ // sibling fallbacks to be rendered first.
+ const suspendedPrimaryTask = createRenderTask(
+ request,
+ null,
+ content,
+ -1,
+ newBoundary,
+ contentRootSegment,
+ newBoundary.contentState,
+ task.abortSet,
+ keyPath,
+ task.formatContext,
+ task.context,
+ task.treeContext,
+ task.componentStack,
+ task.isFallback,
+ !disableLegacyContext ? task.legacyContext : emptyContextObject,
+ __DEV__ && enableOwnerStacks ? task.debugTask : null,
+ );
+ pushComponentStack(suspendedPrimaryTask);
+ request.pingedTasks.push(suspendedPrimaryTask);
+ } else {
+ // This is a normal render. We will attempt to synchronously render the boundary content
+ // If it is successful we will elide the fallback task but if it suspends or errors we schedule
+ // the fallback to render. Unlike with prerenders we attempt to deprioritize the fallback render
+
+ // Currently this is running synchronously. We could instead schedule this to pingedTasks.
+ // I suspect that there might be some efficiency benefits from not creating the suspended task
+ // and instead just using the stack if possible.
+ // TODO: Call this directly instead of messing with saving and restoring contexts.
+
+ // We can reuse the current context and task to render the content immediately without
+ // context switching. We just need to temporarily switch which boundary and which segment
+ // we're writing to. If something suspends, it'll spawn new suspended task with that context.
+ task.blockedBoundary = newBoundary;
+ task.hoistableState = newBoundary.contentState;
+ task.blockedSegment = contentRootSegment;
+ task.keyPath = keyPath;
+
+ try {
+ // We use the safe form because we don't handle suspending here. Only error handling.
+ renderNode(request, task, content, -1);
+ pushSegmentFinale(
+ contentRootSegment.chunks,
+ request.renderState,
+ contentRootSegment.lastPushedText,
+ contentRootSegment.textEmbedded,
+ );
+ contentRootSegment.status = COMPLETED;
+ queueCompletedSegment(newBoundary, contentRootSegment);
+ if (newBoundary.pendingTasks === 0 && newBoundary.status === PENDING) {
+ // 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
+ // the fallback.
+ newBoundary.status = COMPLETED;
+ return;
+ }
+ } catch (error: mixed) {
+ contentRootSegment.status = ERRORED;
+ newBoundary.status = CLIENT_RENDERED;
+ const thrownInfo = getThrownInfo(task.componentStack);
+ let errorDigest;
+ if (
+ enablePostpone &&
+ typeof error === 'object' &&
+ error !== null &&
+ error.$$typeof === REACT_POSTPONE_TYPE
+ ) {
+ const postponeInstance: Postpone = (error: any);
+ logPostpone(
+ request,
+ postponeInstance.message,
+ thrownInfo,
+ __DEV__ && enableOwnerStacks ? task.debugTask : null,
+ );
+ // TODO: Figure out a better signal than a magic digest value.
+ errorDigest = 'POSTPONE';
+ } else {
+ errorDigest = logRecoverableError(
+ request,
+ error,
+ thrownInfo,
+ __DEV__ && enableOwnerStacks ? task.debugTask : null,
+ );
+ }
+ encodeErrorForBoundary(
+ newBoundary,
+ errorDigest,
+ error,
+ thrownInfo,
+ false,
+ );
+
+ untrackBoundary(request, newBoundary);
+
+ // We don't need to decrement any task numbers because we didn't spawn any new task.
+ // We don't need to schedule any task because we know the parent has written yet.
+ // We do need to fallthrough to create the fallback though.
+ } finally {
+ task.blockedBoundary = parentBoundary;
+ task.hoistableState = parentHoistableState;
+ task.blockedSegment = parentSegment;
+ task.keyPath = prevKeyPath;
+ }
+
+ const fallbackKeyPath = [keyPath[0], 'Suspense Fallback', keyPath[2]];
+ // We create suspended task for the fallback because we don't want to actually work
+ // on it yet in case we finish the main content, so we queue for later.
+ const suspendedFallbackTask = createRenderTask(
+ request,
+ null,
+ fallback,
+ -1,
+ parentBoundary,
+ boundarySegment,
+ newBoundary.fallbackState,
+ fallbackAbortSet,
+ fallbackKeyPath,
+ task.formatContext,
+ task.context,
+ task.treeContext,
+ task.componentStack,
+ true,
+ !disableLegacyContext ? task.legacyContext : emptyContextObject,
+ __DEV__ && enableOwnerStacks ? task.debugTask : null,
+ );
+ pushComponentStack(suspendedFallbackTask);
+ // TODO: This should be queued at a separate lower priority queue so that we only work
+ // on preparing fallbacks if we don't have any more main content to task on.
+ request.pingedTasks.push(suspendedFallbackTask);
}
- // We create suspended task for the fallback because we don't want to actually work
- // on it yet in case we finish the main content, so we queue for later.
- const suspendedFallbackTask = createRenderTask(
- request,
- null,
- fallback,
- -1,
- parentBoundary,
- boundarySegment,
- newBoundary.fallbackState,
- fallbackAbortSet,
- fallbackKeyPath,
- task.formatContext,
- task.context,
- task.treeContext,
- task.componentStack,
- true,
- !disableLegacyContext ? task.legacyContext : emptyContextObject,
- __DEV__ && enableOwnerStacks ? task.debugTask : null,
- );
- pushComponentStack(suspendedFallbackTask);
- // TODO: This should be queued at a separate lower priority queue so that we only work
- // on preparing fallbacks if we don't have any more main content to task on.
- request.pingedTasks.push(suspendedFallbackTask);
}
function replaySuspenseBoundary(