Skip to content

Commit

Permalink
Make prerendering always non-blocking (#31056)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
acdlite authored Sep 25, 2024
1 parent 3c7667a commit 0f1856c
Show file tree
Hide file tree
Showing 7 changed files with 298 additions and 137 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -744,7 +744,7 @@ describe('ReactDOMFiberAsync', () => {
// Because it suspended, it remains on the current path
expect(div.textContent).toBe('/path/a');
});
assertLog([]);
assertLog(gate('enableSiblingPrerendering') ? ['Suspend! [/path/b]'] : []);

await act(async () => {
resolvePromise();
Expand Down
5 changes: 4 additions & 1 deletion packages/react-reconciler/src/ReactFiberCompleteWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
enableRenderableContext,
passChildrenWhenCloningPersistedNodes,
disableLegacyMode,
enableSiblingPrerendering,
} from 'shared/ReactFeatureFlags';

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

// Track the lanes that have been scheduled for an immediate retry so that
// we can mark them as suspended upon committing the root.
markSpawnedRetryLane(retryLane);
if (enableSiblingPrerendering) {
markSpawnedRetryLane(retryLane);
}
}
}

Expand Down
32 changes: 20 additions & 12 deletions packages/react-reconciler/src/ReactFiberLane.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
transitionLaneExpirationMs,
retryLaneExpirationMs,
disableLegacyMode,
enableSiblingPrerendering,
} from 'shared/ReactFeatureFlags';
import {isDevToolsPresent} from './ReactFiberDevToolsHook';
import {clz32} from './clz32';
Expand Down Expand Up @@ -270,11 +271,13 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
if (nonIdlePingedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
} else {
// Nothing has been pinged. Check for lanes that need to be prewarmed.
if (!rootHasPendingCommit) {
const lanesToPrewarm = nonIdlePendingLanes & ~warmLanes;
if (lanesToPrewarm !== NoLanes) {
nextLanes = getHighestPriorityLanes(lanesToPrewarm);
if (enableSiblingPrerendering) {
// Nothing has been pinged. Check for lanes that need to be prewarmed.
if (!rootHasPendingCommit) {
const lanesToPrewarm = nonIdlePendingLanes & ~warmLanes;
if (lanesToPrewarm !== NoLanes) {
nextLanes = getHighestPriorityLanes(lanesToPrewarm);
}
}
}
}
Expand All @@ -294,11 +297,13 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
if (pingedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(pingedLanes);
} else {
// Nothing has been pinged. Check for lanes that need to be prewarmed.
if (!rootHasPendingCommit) {
const lanesToPrewarm = pendingLanes & ~warmLanes;
if (lanesToPrewarm !== NoLanes) {
nextLanes = getHighestPriorityLanes(lanesToPrewarm);
if (enableSiblingPrerendering) {
// Nothing has been pinged. Check for lanes that need to be prewarmed.
if (!rootHasPendingCommit) {
const lanesToPrewarm = pendingLanes & ~warmLanes;
if (lanesToPrewarm !== NoLanes) {
nextLanes = getHighestPriorityLanes(lanesToPrewarm);
}
}
}
}
Expand Down Expand Up @@ -760,12 +765,14 @@ export function markRootSuspended(
root: FiberRoot,
suspendedLanes: Lanes,
spawnedLane: Lane,
didSkipSuspendedSiblings: boolean,
didAttemptEntireTree: boolean,
) {
// TODO: Split this into separate functions for marking the root at the end of
// a render attempt versus suspending while the root is still in progress.
root.suspendedLanes |= suspendedLanes;
root.pingedLanes &= ~suspendedLanes;

if (!didSkipSuspendedSiblings) {
if (enableSiblingPrerendering && didAttemptEntireTree) {
// Mark these lanes as warm so we know there's nothing else to work on.
root.warmLanes |= suspendedLanes;
} else {
Expand Down Expand Up @@ -876,6 +883,7 @@ export function markRootFinished(
// suspended) instead of the regular mode (i.e. unwind and skip the siblings
// as soon as something suspends to unblock the rest of the update).
if (
enableSiblingPrerendering &&
suspendedRetryLanes !== NoLanes &&
// Note that we only do this if there were no updates since we started
// rendering. This mirrors the logic in markRootUpdated — whenever we
Expand Down
20 changes: 16 additions & 4 deletions packages/react-reconciler/src/ReactFiberRootScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
disableSchedulerTimeoutInWorkLoop,
enableProfilerTimer,
enableProfilerNestedUpdatePhase,
enableSiblingPrerendering,
} from 'shared/ReactFeatureFlags';
import {
NoLane,
Expand All @@ -29,6 +30,7 @@ import {
markStarvedLanesAsExpired,
claimNextTransitionLane,
getNextLanesToFlushSync,
checkIfRootIsPrerendering,
} from './ReactFiberLane';
import {
CommitContext,
Expand Down Expand Up @@ -206,7 +208,10 @@ function flushSyncWorkAcrossRoots_impl(
? workInProgressRootRenderLanes
: NoLanes,
);
if (includesSyncLane(nextLanes)) {
if (
includesSyncLane(nextLanes) &&
!checkIfRootIsPrerendering(root, nextLanes)
) {
// This root has pending sync work. Flush it now.
didPerformSomeWork = true;
performSyncWorkOnRoot(root, nextLanes);
Expand Down Expand Up @@ -341,7 +346,13 @@ function scheduleTaskForRootDuringMicrotask(
}

// Schedule a new callback in the host environment.
if (includesSyncLane(nextLanes)) {
if (
includesSyncLane(nextLanes) &&
// If we're prerendering, then we should use the concurrent work loop
// even if the lanes are synchronous, so that prerendering never blocks
// the main thread.
!(enableSiblingPrerendering && checkIfRootIsPrerendering(root, nextLanes))
) {
// Synchronous work is always flushed at the end of the microtask, so we
// don't need to schedule an additional task.
if (existingCallbackNode !== null) {
Expand Down Expand Up @@ -375,9 +386,10 @@ function scheduleTaskForRootDuringMicrotask(

let schedulerPriorityLevel;
switch (lanesToEventPriority(nextLanes)) {
// Scheduler does have an "ImmediatePriority", but now that we use
// microtasks for sync work we no longer use that. Any sync work that
// reaches this path is meant to be time sliced.
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediateSchedulerPriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingSchedulerPriority;
break;
Expand Down
Loading

0 comments on commit 0f1856c

Please sign in to comment.