Make prerendering always non-blocking (#31056)
When a synchronous update suspends, and we prerender the siblings, the prerendering should be non-blocking so that we can immediately restart once the data arrives. This happens automatically when there's a Suspense boundary, because we immediately commit the boundary and then proceed to a Retry render, which are always concurrent. When there's not a Suspense boundary, there is no Retry, so we need to take care to switch from the synchronous work loop to the concurrent one, to enable time slicing.
Andrew Clark committed
Sep 25, 2024 at 16:31 UTC
0f1856c49febe96923e469f98c0b123130ea015c
7 files changed
+298
-137
packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js
+1
-1
@@ -744,7 +744,7 @@ describe('ReactDOMFiberAsync', () => {
744
// Because it suspended, it remains on the current path
745
expect(div.textContent).toBe('/path/a');
746
});
747
- assertLog([]);
747
+ assertLog(gate('enableSiblingPrerendering') ? ['Suspend! [/path/b]'] : []);
748
749
await act(async () => {
750
resolvePromise();
packages/react-reconciler/src/ReactFiberCompleteWork.js
+4
-1
@@ -42,6 +42,7 @@ import {
42
enableRenderableContext,
43
passChildrenWhenCloningPersistedNodes,
44
disableLegacyMode,
45
+ enableSiblingPrerendering,
46
} from 'shared/ReactFeatureFlags';
47
48
import {now} from './Scheduler';
@@ -622,7 +623,9 @@ function scheduleRetryEffect(
623
624
// Track the lanes that have been scheduled for an immediate retry so that
625
// we can mark them as suspended upon committing the root.
625
- markSpawnedRetryLane(retryLane);
626
+ if (enableSiblingPrerendering) {
627
+ markSpawnedRetryLane(retryLane);
628
+ }
629
}
630
}
631
packages/react-reconciler/src/ReactFiberLane.js
+20
-12
@@ -27,6 +27,7 @@ import {
27
transitionLaneExpirationMs,
28
retryLaneExpirationMs,
29
disableLegacyMode,
30
+ enableSiblingPrerendering,
31
} from 'shared/ReactFeatureFlags';
32
import {isDevToolsPresent} from './ReactFiberDevToolsHook';
33
import {clz32} from './clz32';
@@ -270,11 +271,13 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
271
if (nonIdlePingedLanes !== NoLanes) {
272
nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
273
} else {
273
- // Nothing has been pinged. Check for lanes that need to be prewarmed.
274
- if (!rootHasPendingCommit) {
275
- const lanesToPrewarm = nonIdlePendingLanes & ~warmLanes;
276
- if (lanesToPrewarm !== NoLanes) {
277
- nextLanes = getHighestPriorityLanes(lanesToPrewarm);
274
+ if (enableSiblingPrerendering) {
275
+ // Nothing has been pinged. Check for lanes that need to be prewarmed.
276
+ if (!rootHasPendingCommit) {
277
+ const lanesToPrewarm = nonIdlePendingLanes & ~warmLanes;
278
+ if (lanesToPrewarm !== NoLanes) {
279
+ nextLanes = getHighestPriorityLanes(lanesToPrewarm);
280
+ }
281
}
282
}
283
}
@@ -294,11 +297,13 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
297
if (pingedLanes !== NoLanes) {
298
nextLanes = getHighestPriorityLanes(pingedLanes);
299
} else {
297
- // Nothing has been pinged. Check for lanes that need to be prewarmed.
298
- if (!rootHasPendingCommit) {
299
- const lanesToPrewarm = pendingLanes & ~warmLanes;
300
- if (lanesToPrewarm !== NoLanes) {
301
- nextLanes = getHighestPriorityLanes(lanesToPrewarm);
300
+ if (enableSiblingPrerendering) {
301
+ // Nothing has been pinged. Check for lanes that need to be prewarmed.
302
+ if (!rootHasPendingCommit) {
303
+ const lanesToPrewarm = pendingLanes & ~warmLanes;
304
+ if (lanesToPrewarm !== NoLanes) {
305
+ nextLanes = getHighestPriorityLanes(lanesToPrewarm);
306
+ }
307
}
308
}
309
}
@@ -760,12 +765,14 @@ export function markRootSuspended(
765
root: FiberRoot,
766
suspendedLanes: Lanes,
767
spawnedLane: Lane,
763
- didSkipSuspendedSiblings: boolean,
768
+ didAttemptEntireTree: boolean,
769
) {
770
+ // TODO: Split this into separate functions for marking the root at the end of
771
+ // a render attempt versus suspending while the root is still in progress.
772
root.suspendedLanes |= suspendedLanes;
773
root.pingedLanes &= ~suspendedLanes;
774
768
- if (!didSkipSuspendedSiblings) {
775
+ if (enableSiblingPrerendering && didAttemptEntireTree) {
776
// Mark these lanes as warm so we know there's nothing else to work on.
777
root.warmLanes |= suspendedLanes;
778
} else {
@@ -876,6 +883,7 @@ export function markRootFinished(
883
// suspended) instead of the regular mode (i.e. unwind and skip the siblings
884
// as soon as something suspends to unblock the rest of the update).
885
if (
886
+ enableSiblingPrerendering &&
887
suspendedRetryLanes !== NoLanes &&
888
// Note that we only do this if there were no updates since we started
889
// rendering. This mirrors the logic in markRootUpdated — whenever we
packages/react-reconciler/src/ReactFiberRootScheduler.js
+16
-4
@@ -18,6 +18,7 @@ import {
18
disableSchedulerTimeoutInWorkLoop,
19
enableProfilerTimer,
20
enableProfilerNestedUpdatePhase,
21
+ enableSiblingPrerendering,
22
} from 'shared/ReactFeatureFlags';
23
import {
24
NoLane,
@@ -29,6 +30,7 @@ import {
30
markStarvedLanesAsExpired,
31
claimNextTransitionLane,
32
getNextLanesToFlushSync,
33
+ checkIfRootIsPrerendering,
34
} from './ReactFiberLane';
35
import {
36
CommitContext,
@@ -206,7 +208,10 @@ function flushSyncWorkAcrossRoots_impl(
208
? workInProgressRootRenderLanes
209
: NoLanes,
210
);
209
- if (includesSyncLane(nextLanes)) {
211
+ if (
212
+ includesSyncLane(nextLanes) &&
213
+ !checkIfRootIsPrerendering(root, nextLanes)
214
+ ) {
215
// This root has pending sync work. Flush it now.
216
didPerformSomeWork = true;
217
performSyncWorkOnRoot(root, nextLanes);
@@ -341,7 +346,13 @@ function scheduleTaskForRootDuringMicrotask(
346
}
347
348
// Schedule a new callback in the host environment.
344
- if (includesSyncLane(nextLanes)) {
349
+ if (
350
+ includesSyncLane(nextLanes) &&
351
+ // If we're prerendering, then we should use the concurrent work loop
352
+ // even if the lanes are synchronous, so that prerendering never blocks
353
+ // the main thread.
354
+ !(enableSiblingPrerendering && checkIfRootIsPrerendering(root, nextLanes))
355
+ ) {
356
// Synchronous work is always flushed at the end of the microtask, so we
357
// don't need to schedule an additional task.
358
if (existingCallbackNode !== null) {
@@ -375,9 +386,10 @@ function scheduleTaskForRootDuringMicrotask(
386
387
let schedulerPriorityLevel;
388
switch (lanesToEventPriority(nextLanes)) {
389
+ // Scheduler does have an "ImmediatePriority", but now that we use
390
+ // microtasks for sync work we no longer use that. Any sync work that
391
+ // reaches this path is meant to be time sliced.
392
case DiscreteEventPriority:
379
- schedulerPriorityLevel = ImmediateSchedulerPriority;
380
- break;
393
case ContinuousEventPriority:
394
schedulerPriorityLevel = UserBlockingSchedulerPriority;
395
break;
packages/react-reconciler/src/ReactFiberWorkLoop.js
+174
-119
@@ -765,11 +765,12 @@ export function scheduleUpdateOnFiber(
765
// The incoming update might unblock the current render. Interrupt the
766
// current attempt and restart from the top.
767
prepareFreshStack(root, NoLanes);
768
+ const didAttemptEntireTree = false;
769
markRootSuspended(
770
root,
771
workInProgressRootRenderLanes,
772
workInProgressDeferredLane,
772
- workInProgressRootDidSkipSuspendedSiblings,
773
+ didAttemptEntireTree,
774
);
775
}
776
@@ -832,11 +833,12 @@ export function scheduleUpdateOnFiber(
833
// effect of interrupting the current render and switching to the update.
834
// TODO: Make sure this doesn't override pings that happen while we've
835
// already started rendering.
836
+ const didAttemptEntireTree = false;
837
markRootSuspended(
838
root,
839
workInProgressRootRenderLanes,
840
workInProgressDeferredLane,
839
- workInProgressRootDidSkipSuspendedSiblings,
841
+ didAttemptEntireTree,
842
);
843
}
844
}
@@ -898,100 +900,120 @@ export function performWorkOnRoot(
900
// for too long ("expired" work, to prevent starvation), or we're in
901
// sync-updates-by-default mode.
902
const shouldTimeSlice =
901
- !forceSync &&
902
- !includesBlockingLane(lanes) &&
903
- !includesExpiredLane(root, lanes);
903
+ (!forceSync &&
904
+ !includesBlockingLane(lanes) &&
905
+ !includesExpiredLane(root, lanes)) ||
906
+ // If we're prerendering, then we should use the concurrent work loop
907
+ // even if the lanes are synchronous, so that prerendering never blocks
908
+ // the main thread.
909
+ // TODO: We should consider doing this whenever a sync lane is suspended,
910
+ // even for regular pings.
911
+ (enableSiblingPrerendering && checkIfRootIsPrerendering(root, lanes));
912
+
913
let exitStatus = shouldTimeSlice
914
? renderRootConcurrent(root, lanes)
906
- : renderRootSync(root, lanes);
915
+ : renderRootSync(root, lanes, true);
916
908
- if (exitStatus !== RootInProgress) {
917
+ do {
918
let renderWasConcurrent = shouldTimeSlice;
910
- do {
911
- if (exitStatus === RootDidNotComplete) {
912
- // The render unwound without completing the tree. This happens in special
913
- // cases where need to exit the current render without producing a
914
- // consistent tree or committing.
915
- markRootSuspended(
919
+ if (exitStatus === RootInProgress) {
920
+ // Render phase is still in progress.
921
+ if (
922
+ enableSiblingPrerendering &&
923
+ workInProgressRootIsPrerendering &&
924
+ !shouldTimeSlice
925
+ ) {
926
+ // We're in prerendering mode, but time slicing is not enabled. This
927
+ // happens when something suspends during a synchronous update. Exit the
928
+ // the work loop. When we resume, we'll use the concurrent work loop so
929
+ // that prerendering is non-blocking.
930
+ //
931
+ // Mark the root as suspended. Usually we do this at the end of the
932
+ // render phase, but we do it here so that we resume in
933
+ // prerendering mode.
934
+ // TODO: Consider always calling markRootSuspended immediately.
935
+ // Needs to be *after* we attach a ping listener, though.
936
+ const didAttemptEntireTree = false;
937
+ markRootSuspended(root, lanes, NoLane, didAttemptEntireTree);
938
+ }
939
+ break;
940
+ } else if (exitStatus === RootDidNotComplete) {
941
+ // The render unwound without completing the tree. This happens in special
942
+ // cases where need to exit the current render without producing a
943
+ // consistent tree or committing.
944
+ const didAttemptEntireTree = !workInProgressRootDidSkipSuspendedSiblings;
945
+ markRootSuspended(root, lanes, NoLane, didAttemptEntireTree);
946
+ } else {
947
+ // The render completed.
948
+
949
+ // Check if this render may have yielded to a concurrent event, and if so,
950
+ // confirm that any newly rendered stores are consistent.
951
+ // TODO: It's possible that even a concurrent render may never have yielded
952
+ // to the main thread, if it was fast enough, or if it expired. We could
953
+ // skip the consistency check in that case, too.
954
+ const finishedWork: Fiber = (root.current.alternate: any);
955
+ if (
956
+ renderWasConcurrent &&
957
+ !isRenderConsistentWithExternalStores(finishedWork)
958
+ ) {
959
+ // A store was mutated in an interleaved event. Render again,
960
+ // synchronously, to block further mutations.
961
+ exitStatus = renderRootSync(root, lanes, false);
962
+ // We assume the tree is now consistent because we didn't yield to any
963
+ // concurrent events.
964
+ renderWasConcurrent = false;
965
+ // Need to check the exit status again.
966
+ continue;
967
+ }
968
+
969
+ // Check if something threw
970
+ if (
971
+ (disableLegacyMode || root.tag !== LegacyRoot) &&
972
+ exitStatus === RootErrored
973
+ ) {
974
+ const lanesThatJustErrored = lanes;
975
+ const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
976
root,
917
- lanes,
918
- NoLane,
919
- workInProgressRootDidSkipSuspendedSiblings,
977
+ lanesThatJustErrored,
978
);
921
- } else {
922
- // The render completed.
923
-
924
- // Check if this render may have yielded to a concurrent event, and if so,
925
- // confirm that any newly rendered stores are consistent.
926
- // TODO: It's possible that even a concurrent render may never have yielded
927
- // to the main thread, if it was fast enough, or if it expired. We could
928
- // skip the consistency check in that case, too.
929
- const finishedWork: Fiber = (root.current.alternate: any);
930
- if (
931
- renderWasConcurrent &&
932
- !isRenderConsistentWithExternalStores(finishedWork)
933
- ) {
934
- // A store was mutated in an interleaved event. Render again,
935
- // synchronously, to block further mutations.
936
- exitStatus = renderRootSync(root, lanes);
937
- // We assume the tree is now consistent because we didn't yield to any
938
- // concurrent events.
939
- renderWasConcurrent = false;
940
- // Need to check the exit status again.
941
- continue;
942
- }
943
-
944
- // Check if something threw
945
- if (
946
- (disableLegacyMode || root.tag !== LegacyRoot) &&
947
- exitStatus === RootErrored
948
- ) {
949
- const lanesThatJustErrored = lanes;
950
- const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
979
+ if (errorRetryLanes !== NoLanes) {
980
+ lanes = errorRetryLanes;
981
+ exitStatus = recoverFromConcurrentError(
982
root,
983
lanesThatJustErrored,
984
+ errorRetryLanes,
985
);
954
- if (errorRetryLanes !== NoLanes) {
955
- lanes = errorRetryLanes;
956
- exitStatus = recoverFromConcurrentError(
957
- root,
958
- lanesThatJustErrored,
959
- errorRetryLanes,
960
- );
961
- renderWasConcurrent = false;
962
- // Need to check the exit status again.
963
- if (exitStatus !== RootErrored) {
964
- // The root did not error this time. Restart the exit algorithm
965
- // from the beginning.
966
- // TODO: Refactor the exit algorithm to be less confusing. Maybe
967
- // more branches + recursion instead of a loop. I think the only
968
- // thing that causes it to be a loop is the RootDidNotComplete
969
- // check. If that's true, then we don't need a loop/recursion
970
- // at all.
971
- continue;
972
- } else {
973
- // The root errored yet again. Proceed to commit the tree.
974
- }
986
+ renderWasConcurrent = false;
987
+ // Need to check the exit status again.
988
+ if (exitStatus !== RootErrored) {
989
+ // The root did not error this time. Restart the exit algorithm
990
+ // from the beginning.
991
+ // TODO: Refactor the exit algorithm to be less confusing. Maybe
992
+ // more branches + recursion instead of a loop. I think the only
993
+ // thing that causes it to be a loop is the RootDidNotComplete
994
+ // check. If that's true, then we don't need a loop/recursion
995
+ // at all.
996
+ continue;
997
+ } else {
998
+ // The root errored yet again. Proceed to commit the tree.
999
}
1000
}
977
- if (exitStatus === RootFatalErrored) {
978
- prepareFreshStack(root, NoLanes);
979
- markRootSuspended(
980
- root,
981
- lanes,
982
- NoLane,
983
- workInProgressRootDidSkipSuspendedSiblings,
984
- );
985
- break;
986
- }
987
-
988
- // We now have a consistent tree. The next step is either to commit it,
989
- // or, if something suspended, wait to commit it after a timeout.
990
- finishConcurrentRender(root, exitStatus, finishedWork, lanes);
1001
}
992
- break;
993
- } while (true);
994
- }
1002
+ if (exitStatus === RootFatalErrored) {
1003
+ prepareFreshStack(root, NoLanes);
1004
+ // Since this is a fatal error, we're going to pretend we attempted
1005
+ // the entire tree, to avoid scheduling a prerender.
1006
+ const didAttemptEntireTree = true;
1007
+ markRootSuspended(root, lanes, NoLane, didAttemptEntireTree);
1008
+ break;
1009
+ }
1010
+
1011
+ // We now have a consistent tree. The next step is either to commit it,
1012
+ // or, if something suspended, wait to commit it after a timeout.
1013
+ finishConcurrentRender(root, exitStatus, finishedWork, lanes);
1014
+ }
1015
+ break;
1016
+ } while (true);
1017
1018
ensureRootIsScheduled(root);
1019
}
@@ -1024,7 +1046,7 @@ function recoverFromConcurrentError(
1046
rootWorkInProgress.flags |= ForceClientRender;
1047
}
1048
1027
- const exitStatus = renderRootSync(root, errorRetryLanes);
1049
+ const exitStatus = renderRootSync(root, errorRetryLanes, false);
1050
if (exitStatus !== RootErrored) {
1051
// Successfully finished rendering on retry
1052
@@ -1108,11 +1130,13 @@ function finishConcurrentRender(
1130
// This is a transition, so we should exit without committing a
1131
// placeholder and without scheduling a timeout. Delay indefinitely
1132
// until we receive more data.
1133
+ const didAttemptEntireTree =
1134
+ !workInProgressRootDidSkipSuspendedSiblings;
1135
markRootSuspended(
1136
root,
1137
lanes,
1138
workInProgressDeferredLane,
1115
- workInProgressRootDidSkipSuspendedSiblings,
1139
+ didAttemptEntireTree,
1140
);
1141
return;
1142
}
@@ -1168,11 +1192,13 @@ function finishConcurrentRender(
1192
1193
// Don't bother with a very short suspense time.
1194
if (msUntilTimeout > 10) {
1195
+ const didAttemptEntireTree =
1196
+ !workInProgressRootDidSkipSuspendedSiblings;
1197
markRootSuspended(
1198
root,
1199
lanes,
1200
workInProgressDeferredLane,
1175
- workInProgressRootDidSkipSuspendedSiblings,
1201
+ didAttemptEntireTree,
1202
);
1203
1204
const nextLanes = getNextLanes(root, NoLanes);
@@ -1284,7 +1310,8 @@ function commitRootWhenReady(
1310
SUSPENDED_COMMIT,
1311
),
1312
);
1287
- markRootSuspended(root, lanes, spawnedLane, didSkipSuspendedSiblings);
1313
+ const didAttemptEntireTree = !didSkipSuspendedSiblings;
1314
+ markRootSuspended(root, lanes, spawnedLane, didAttemptEntireTree);
1315
return;
1316
}
1317
}
@@ -1407,7 +1434,7 @@ function markRootSuspended(
1434
root: FiberRoot,
1435
suspendedLanes: Lanes,
1436
spawnedLane: Lane,
1410
- didSkipSuspendedSiblings: boolean,
1437
+ didAttemptEntireTree: boolean,
1438
) {
1439
// When suspending, we should always exclude lanes that were pinged or (more
1440
// rarely, since we try to avoid it) updated during the render phase.
@@ -1416,12 +1443,7 @@ function markRootSuspended(
1443
suspendedLanes,
1444
workInProgressRootInterleavedUpdatedLanes,
1445
);
1419
- _markRootSuspended(
1420
- root,
1421
- suspendedLanes,
1422
- spawnedLane,
1423
- didSkipSuspendedSiblings,
1424
- );
1446
+ _markRootSuspended(root, suspendedLanes, spawnedLane, didAttemptEntireTree);
1447
}
1448
1449
export function flushRoot(root: FiberRoot, lanes: Lanes) {
@@ -1963,7 +1985,12 @@ export function renderDidSuspendDelayIfPossible(): void {
1985
1986
if (
1987
!workInProgressRootDidSkipSuspendedSiblings &&
1966
- !includesBlockingLane(workInProgressRootRenderLanes)
1988
+ // Check if the root will be blocked from committing.
1989
+ // TODO: Consider aligning this better with the rest of the logic. Maybe
1990
+ // we should only set the exit status to RootSuspendedWithDelay if this
1991
+ // condition is true? And remove the equivalent checks elsewhere.
1992
+ (includesOnlyTransitions(workInProgressRootRenderLanes) ||
1993
+ getSuspenseHandler() === null)
1994
) {
1995
// This render may not have originally been scheduled as a prerender, but
1996
// something suspended inside the visible part of the tree, which means we
@@ -1989,11 +2016,12 @@ export function renderDidSuspendDelayIfPossible(): void {
2016
// pinged or updated while we were rendering.
2017
// TODO: Consider unwinding immediately, using the
2018
// SuspendedOnHydration mechanism.
2019
+ const didAttemptEntireTree = false;
2020
markRootSuspended(
2021
workInProgressRoot,
2022
workInProgressRootRenderLanes,
2023
workInProgressDeferredLane,
1996
- workInProgressRootDidSkipSuspendedSiblings,
2024
+ didAttemptEntireTree,
2025
);
2026
}
2027
}
@@ -2023,7 +2051,11 @@ export function renderHasNotSuspendedYet(): boolean {
2051
// TODO: Over time, this function and renderRootConcurrent have become more
2052
// and more similar. Not sure it makes sense to maintain forked paths. Consider
2053
// unifying them again.
2026
-function renderRootSync(root: FiberRoot, lanes: Lanes) {
2054
+function renderRootSync(
2055
+ root: FiberRoot,
2056
+ lanes: Lanes,
2057
+ shouldYieldForPrerendering: boolean,
2058
+): RootExitStatus {
2059
const prevExecutionContext = executionContext;
2060
executionContext |= RenderContext;
2061
const prevDispatcher = pushDispatcher(root.containerInfo);
@@ -2063,6 +2095,7 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
2095
}
2096
2097
let didSuspendInShell = false;
2098
+ let exitStatus = workInProgressRootExitStatus;
2099
outer: do {
2100
try {
2101
if (
@@ -2084,16 +2117,37 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
2117
// Selective hydration. An update flowed into a dehydrated tree.
2118
// Interrupt the current render so the work loop can switch to the
2119
// hydration lane.
2120
+ // TODO: I think we might not need to reset the stack here; we can
2121
+ // just yield and reset the stack when we re-enter the work loop,
2122
+ // like normal.
2123
resetWorkInProgressStack();
2088
- workInProgressRootExitStatus = RootDidNotComplete;
2124
+ exitStatus = RootDidNotComplete;
2125
break outer;
2126
}
2127
case SuspendedOnImmediate:
2092
- case SuspendedOnData: {
2093
- if (!didSuspendInShell && getSuspenseHandler() === null) {
2128
+ case SuspendedOnData:
2129
+ case SuspendedOnDeprecatedThrowPromise: {
2130
+ if (getSuspenseHandler() === null) {
2131
didSuspendInShell = true;
2132
}
2096
- // Intentional fallthrough
2133
+ const reason = workInProgressSuspendedReason;
2134
+ workInProgressSuspendedReason = NotSuspended;
2135
+ workInProgressThrownValue = null;
2136
+ throwAndUnwindWorkLoop(root, unitOfWork, thrownValue, reason);
2137
+ if (
2138
+ enableSiblingPrerendering &&
2139
+ shouldYieldForPrerendering &&
2140
+ workInProgressRootIsPrerendering
2141
+ ) {
2142
+ // We've switched into prerendering mode. This implies that we
2143
+ // suspended outside of a Suspense boundary, which means this
2144
+ // render will be blocked from committing. Yield to the main
2145
+ // thread so we can switch to prerendering using the concurrent
2146
+ // work loop.
2147
+ exitStatus = RootInProgress;
2148
+ break outer;
2149
+ }
2150
+ break;
2151
}
2152
default: {
2153
// Unwind then continue with the normal work loop.
@@ -2106,6 +2160,7 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
2160
}
2161
}
2162
workLoopSync();
2163
+ exitStatus = workInProgressRootExitStatus;
2164
break;
2165
} catch (thrownValue) {
2166
handleThrow(root, thrownValue);
@@ -2128,14 +2183,6 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
2183
popDispatcher(prevDispatcher);
2184
popAsyncDispatcher(prevAsyncDispatcher);
2185
2131
- if (workInProgress !== null) {
2132
- // This is a sync render, so we should have finished the whole tree.
2133
- throw new Error(
2134
- 'Cannot commit an incomplete root. This error is likely caused by a ' +
2135
- 'bug in React. Please file an issue.',
2136
- );
2137
- }
2138
-
2186
if (__DEV__) {
2187
if (enableDebugTracing) {
2188
logRenderStopped();
@@ -2146,14 +2193,21 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
2193
markRenderStopped();
2194
}
2195
2149
- // Set this to null to indicate there's no in-progress render.
2150
- workInProgressRoot = null;
2151
- workInProgressRootRenderLanes = NoLanes;
2196
+ if (workInProgress !== null) {
2197
+ // Did not complete the tree. This can happen if something suspended in
2198
+ // the shell.
2199
+ } else {
2200
+ // Normal case. We completed the whole tree.
2201
+
2202
+ // Set this to null to indicate there's no in-progress render.
2203
+ workInProgressRoot = null;
2204
+ workInProgressRootRenderLanes = NoLanes;
2205
2153
- // It's safe to process the queue now that the render phase is complete.
2154
- finishQueueingConcurrentUpdates();
2206
+ // It's safe to process the queue now that the render phase is complete.
2207
+ finishQueueingConcurrentUpdates();
2208
+ }
2209
2156
- return workInProgressRootExitStatus;
2210
+ return exitStatus;
2211
}
2212
2213
// The work loop is an extremely hot path. Tell Closure not to inline it.
@@ -2199,9 +2253,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2253
//
2254
// If we were previously in prerendering mode, check if we received any new
2255
// data during an interleaved event.
2202
- if (workInProgressRootIsPrerendering) {
2203
- workInProgressRootIsPrerendering = checkIfRootIsPrerendering(root, lanes);
2204
- }
2256
+ workInProgressRootIsPrerendering = checkIfRootIsPrerendering(root, lanes);
2257
}
2258
2259
if (__DEV__) {
@@ -3753,6 +3805,9 @@ function pingSuspendedRoot(
3805
// the logic of whether or not a root suspends once it completes.
3806
// TODO: If we're rendering sync either due to Sync, Batched or expired,
3807
// we should probably never restart.
3808
+ // TODO: Attach different listeners depending on whether the listener was
3809
+ // attached during prerendering. Prerender pings should not interrupt
3810
+ // normal renders.
3811
3812
// If we're suspended with delay, or if it's a retry, we'll always suspend
3813
// so we can always restart.
packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js
+12
@@ -420,6 +420,10 @@ describe('ReactDeferredValue', () => {
420
// The initial value suspended, so we attempt the final value, which
421
// also suspends.
422
'Suspend! [Final]',
423
+
424
+ ...(gate('enableSiblingPrerendering')
425
+ ? ['Suspend! [Loading...]', 'Suspend! [Final]']
426
+ : []),
427
]);
428
expect(root).toMatchRenderedOutput(null);
429
@@ -459,6 +463,10 @@ describe('ReactDeferredValue', () => {
463
// The initial value suspended, so we attempt the final value, which
464
// also suspends.
465
'Suspend! [Final]',
466
+
467
+ ...(gate('enableSiblingPrerendering')
468
+ ? ['Suspend! [Loading...]', 'Suspend! [Final]']
469
+ : []),
470
]);
471
expect(root).toMatchRenderedOutput(null);
472
@@ -533,6 +541,10 @@ describe('ReactDeferredValue', () => {
541
// The initial value suspended, so we attempt the final value, which
542
// also suspends.
543
'Suspend! [Final]',
544
+
545
+ ...(gate('enableSiblingPrerendering')
546
+ ? ['Suspend! [Loading...]', 'Suspend! [Final]']
547
+ : []),
548
]);
549
expect(root).toMatchRenderedOutput(null);
550
packages/react-reconciler/src/__tests__/ReactSiblingPrerendering-test.js
+71
@@ -479,4 +479,75 @@ describe('ReactSiblingPrerendering', () => {
479
assertLog([]);
480
},
481
);
482
+
483
+ it(
484
+ 'when a synchronous update suspends outside a boundary, the resulting' +
485
+ 'prerender is concurrent',
486
+ async () => {
487
+ function App() {
488
+ return (
489
+ <>
490
+ <Text text="A" />
491
+ <Text text="B" />
492
+ <AsyncText text="Async" />
493
+ <Text text="C" />
494
+ <Text text="D" />
495
+ </>
496
+ );
497
+ }
498
+
499
+ const root = ReactNoop.createRoot();
500
+ // Mount the root synchronously
501
+ ReactNoop.flushSync(() => root.render(<App />));
502
+
503
+ // Synchronously render everything until we suspend in the shell
504
+ assertLog(['A', 'B', 'Suspend! [Async]']);
505
+
506
+ if (gate('enableSiblingPrerendering')) {
507
+ // The rest of the siblings begin to prerender concurrently. Notice
508
+ // that we don't unwind here; we pick up where we left off above.
509
+ await waitFor(['C']);
510
+ await waitFor(['D']);
511
+ }
512
+
513
+ assertLog([]);
514
+ expect(root).toMatchRenderedOutput(null);
515
+
516
+ await resolveText('Async');
517
+ assertLog(['A', 'B', 'Async', 'C', 'D']);
518
+ expect(root).toMatchRenderedOutput('ABAsyncCD');
519
+ },
520
+ );
521
+
522
+ it('restart a suspended sync render if something suspends while prerendering the siblings', async () => {
523
+ function App() {
524
+ return (
525
+ <>
526
+ <Text text="A" />
527
+ <Text text="B" />
528
+ <AsyncText text="Async" />
529
+ <Text text="C" />
530
+ <Text text="D" />
531
+ </>
532
+ );
533
+ }
534
+
535
+ const root = ReactNoop.createRoot();
536
+ // Mount the root synchronously
537
+ ReactNoop.flushSync(() => root.render(<App />));
538
+
539
+ // Synchronously render everything until we suspend in the shell
540
+ assertLog(['A', 'B', 'Suspend! [Async]']);
541
+
542
+ if (gate('enableSiblingPrerendering')) {
543
+ // The rest of the siblings begin to prerender concurrently
544
+ await waitFor(['C']);
545
+ }
546
+
547
+ // While we're prerendering, Async resolves. We should unwind and
548
+ // start over, rather than continue prerendering D.
549
+ await resolveText('Async');
550
+ assertLog(['A', 'B', 'Async', 'C', 'D']);
551
+ expect(root).toMatchRenderedOutput('ABAsyncCD');
552
+ });
553
});