Skip to content

Commit 0f1856c

Browse files
authored
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.
1 parent 3c7667a commit 0f1856c

File tree

7 files changed

+298
-137
lines changed

7 files changed

+298
-137
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -744,7 +744,7 @@ describe('ReactDOMFiberAsync', () => {
744744
// Because it suspended, it remains on the current path
745745
expect(div.textContent).toBe('/path/a');
746746
});
747-
assertLog([]);
747+
assertLog(gate('enableSiblingPrerendering') ? ['Suspend! [/path/b]'] : []);
748748

749749
await act(async () => {
750750
resolvePromise();

packages/react-reconciler/src/ReactFiberCompleteWork.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
enableRenderableContext,
4343
passChildrenWhenCloningPersistedNodes,
4444
disableLegacyMode,
45+
enableSiblingPrerendering,
4546
} from 'shared/ReactFeatureFlags';
4647

4748
import {now} from './Scheduler';
@@ -622,7 +623,9 @@ function scheduleRetryEffect(
622623

623624
// Track the lanes that have been scheduled for an immediate retry so that
624625
// we can mark them as suspended upon committing the root.
625-
markSpawnedRetryLane(retryLane);
626+
if (enableSiblingPrerendering) {
627+
markSpawnedRetryLane(retryLane);
628+
}
626629
}
627630
}
628631

packages/react-reconciler/src/ReactFiberLane.js

+20-12
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
transitionLaneExpirationMs,
2828
retryLaneExpirationMs,
2929
disableLegacyMode,
30+
enableSiblingPrerendering,
3031
} from 'shared/ReactFeatureFlags';
3132
import {isDevToolsPresent} from './ReactFiberDevToolsHook';
3233
import {clz32} from './clz32';
@@ -270,11 +271,13 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
270271
if (nonIdlePingedLanes !== NoLanes) {
271272
nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
272273
} 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+
}
278281
}
279282
}
280283
}
@@ -294,11 +297,13 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
294297
if (pingedLanes !== NoLanes) {
295298
nextLanes = getHighestPriorityLanes(pingedLanes);
296299
} 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+
}
302307
}
303308
}
304309
}
@@ -760,12 +765,14 @@ export function markRootSuspended(
760765
root: FiberRoot,
761766
suspendedLanes: Lanes,
762767
spawnedLane: Lane,
763-
didSkipSuspendedSiblings: boolean,
768+
didAttemptEntireTree: boolean,
764769
) {
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.
765772
root.suspendedLanes |= suspendedLanes;
766773
root.pingedLanes &= ~suspendedLanes;
767774

768-
if (!didSkipSuspendedSiblings) {
775+
if (enableSiblingPrerendering && didAttemptEntireTree) {
769776
// Mark these lanes as warm so we know there's nothing else to work on.
770777
root.warmLanes |= suspendedLanes;
771778
} else {
@@ -876,6 +883,7 @@ export function markRootFinished(
876883
// suspended) instead of the regular mode (i.e. unwind and skip the siblings
877884
// as soon as something suspends to unblock the rest of the update).
878885
if (
886+
enableSiblingPrerendering &&
879887
suspendedRetryLanes !== NoLanes &&
880888
// Note that we only do this if there were no updates since we started
881889
// rendering. This mirrors the logic in markRootUpdated — whenever we

packages/react-reconciler/src/ReactFiberRootScheduler.js

+16-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
disableSchedulerTimeoutInWorkLoop,
1919
enableProfilerTimer,
2020
enableProfilerNestedUpdatePhase,
21+
enableSiblingPrerendering,
2122
} from 'shared/ReactFeatureFlags';
2223
import {
2324
NoLane,
@@ -29,6 +30,7 @@ import {
2930
markStarvedLanesAsExpired,
3031
claimNextTransitionLane,
3132
getNextLanesToFlushSync,
33+
checkIfRootIsPrerendering,
3234
} from './ReactFiberLane';
3335
import {
3436
CommitContext,
@@ -206,7 +208,10 @@ function flushSyncWorkAcrossRoots_impl(
206208
? workInProgressRootRenderLanes
207209
: NoLanes,
208210
);
209-
if (includesSyncLane(nextLanes)) {
211+
if (
212+
includesSyncLane(nextLanes) &&
213+
!checkIfRootIsPrerendering(root, nextLanes)
214+
) {
210215
// This root has pending sync work. Flush it now.
211216
didPerformSomeWork = true;
212217
performSyncWorkOnRoot(root, nextLanes);
@@ -341,7 +346,13 @@ function scheduleTaskForRootDuringMicrotask(
341346
}
342347

343348
// 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+
) {
345356
// Synchronous work is always flushed at the end of the microtask, so we
346357
// don't need to schedule an additional task.
347358
if (existingCallbackNode !== null) {
@@ -375,9 +386,10 @@ function scheduleTaskForRootDuringMicrotask(
375386

376387
let schedulerPriorityLevel;
377388
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.
378392
case DiscreteEventPriority:
379-
schedulerPriorityLevel = ImmediateSchedulerPriority;
380-
break;
381393
case ContinuousEventPriority:
382394
schedulerPriorityLevel = UserBlockingSchedulerPriority;
383395
break;

0 commit comments

Comments
 (0)