Skip to content

Commit

Permalink
useDeferredValue switches to final if initial suspends
Browse files Browse the repository at this point in the history
If a parent render spawns a deferred task with useDeferredValue, but
the parent render suspends, we should not wait for the parent render to
complete before attempting to render the final value.

The reason is that the initialValue argument to useDeferredValue is
meant to represent an immediate preview of the final UI. If we can't
render it "immediately", we might as well skip it and go straight to
the "real" value.

This is an improvement over how a userspace implementation of
useDeferredValue would work, because a userspace implementation would
have to wait for the parent task to commit (useEffect) before spawning
the deferred task, creating a waterfall.
  • Loading branch information
acdlite committed Oct 17, 2023
1 parent d6443ed commit 508437f
Show file tree
Hide file tree
Showing 7 changed files with 438 additions and 66 deletions.

Large diffs are not rendered by default.

24 changes: 12 additions & 12 deletions packages/react-devtools-shared/src/__tests__/preprocessData-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2001,15 +2001,15 @@ describe('Timeline profiler', () => {
524288 => "Transition",
1048576 => "Transition",
2097152 => "Transition",
4194304 => "Transition",
4194304 => "Retry",
8388608 => "Retry",
16777216 => "Retry",
33554432 => "Retry",
67108864 => "Retry",
134217728 => "SelectiveHydration",
268435456 => "IdleHydration",
536870912 => "Idle",
1073741824 => "Offscreen",
67108864 => "SelectiveHydration",
134217728 => "IdleHydration",
268435456 => "Idle",
536870912 => "Offscreen",
1073741824 => "Deferred",
},
"laneToReactMeasureMap": Map {
1 => [],
Expand Down Expand Up @@ -2269,15 +2269,15 @@ describe('Timeline profiler', () => {
524288 => "Transition",
1048576 => "Transition",
2097152 => "Transition",
4194304 => "Transition",
4194304 => "Retry",
8388608 => "Retry",
16777216 => "Retry",
33554432 => "Retry",
67108864 => "Retry",
134217728 => "SelectiveHydration",
268435456 => "IdleHydration",
536870912 => "Idle",
1073741824 => "Offscreen",
67108864 => "SelectiveHydration",
134217728 => "IdleHydration",
268435456 => "Idle",
536870912 => "Offscreen",
1073741824 => "Deferred",
},
"laneToReactMeasureMap": Map {
1 => [],
Expand Down
23 changes: 15 additions & 8 deletions packages/react-reconciler/src/ReactFiberHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,17 @@ import {
NoLane,
SyncLane,
OffscreenLane,
DeferredLane,
NoLanes,
isSubsetOfLanes,
includesBlockingLane,
includesOnlyNonUrgentLanes,
claimNextTransitionLane,
mergeLanes,
removeLanes,
intersectLanes,
isTransitionLane,
markRootEntangled,
includesSomeLane,
} from './ReactFiberLane';
import {
ContinuousEventPriority,
Expand Down Expand Up @@ -101,6 +102,7 @@ import {
getWorkInProgressRootRenderLanes,
scheduleUpdateOnFiber,
requestUpdateLane,
requestDeferredLane,
markSkippedUpdateLanes,
isInvalidExecutionContextForEventFunction,
} from './ReactFiberWorkLoop';
Expand Down Expand Up @@ -2665,16 +2667,21 @@ function rerenderDeferredValue<T>(value: T, initialValue?: T): T {
}

function mountDeferredValueImpl<T>(hook: Hook, value: T, initialValue?: T): T {
if (enableUseDeferredValueInitialArg && initialValue !== undefined) {
if (
enableUseDeferredValueInitialArg &&
// When `initialValue` is provided, we defer the initial render even if the
// current render is not synchronous.
// TODO: However, to avoid waterfalls, we should not defer if this render
// was itself spawned by an earlier useDeferredValue. Plan is to add a
// Deferred lane to track this.
initialValue !== undefined &&
// However, to avoid waterfalls, we do not defer if this render
// was itself spawned by an earlier useDeferredValue. Check if DeferredLane
// is part of the render lanes.
!includesSomeLane(renderLanes, DeferredLane)
) {
// Render with the initial value
hook.memoizedState = initialValue;

// Schedule a deferred render
const deferredLane = claimNextTransitionLane();
// Schedule a deferred render to switch to the final value.
const deferredLane = requestDeferredLane();
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
deferredLane,
Expand Down Expand Up @@ -2710,7 +2717,7 @@ function updateDeferredValueImpl<T>(

if (!is(value, prevValue)) {
// Schedule a deferred render
const deferredLane = claimNextTransitionLane();
const deferredLane = requestDeferredLane();
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
deferredLane,
Expand Down
47 changes: 45 additions & 2 deletions packages/react-reconciler/src/ReactFiberLane.js
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,11 @@ export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
}
}

export function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {
export function markRootSuspended(
root: FiberRoot,
suspendedLanes: Lanes,
spawnedLane: Lane,
) {
root.suspendedLanes |= suspendedLanes;
root.pingedLanes &= ~suspendedLanes;

Expand All @@ -637,13 +641,21 @@ export function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {

lanes &= ~lane;
}

if (spawnedLane !== NoLane) {
markSpawnedDeferredLane(root, spawnedLane, suspendedLanes);
}
}

export function markRootPinged(root: FiberRoot, pingedLanes: Lanes) {
root.pingedLanes |= root.suspendedLanes & pingedLanes;
}

export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
export function markRootFinished(
root: FiberRoot,
remainingLanes: Lanes,
spawnedLane: Lane,
) {
const noLongerPendingLanes = root.pendingLanes & ~remainingLanes;

root.pendingLanes = remainingLanes;
Expand Down Expand Up @@ -689,6 +701,37 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {

lanes &= ~lane;
}

if (spawnedLane !== NoLane) {
markSpawnedDeferredLane(
root,
spawnedLane,
// This render finished successfully without suspending, so we don't need
// to entangle the spawned task with the parent task.
NoLanes,
);
}
}

function markSpawnedDeferredLane(
root: FiberRoot,
spawnedLane: Lane,
entangledLanes: Lanes,
) {
// This render spawned a deferred task. Mark it as pending.
root.pendingLanes |= spawnedLane;
root.suspendedLanes &= ~spawnedLane;

// Entangle the spawned lane with the DeferredLane bit so that we know it
// was the result of another render. This lets us avoid a useDeferredValue
// waterfall — only the first level will defer.
const spawnedLaneIndex = laneToIndex(spawnedLane);
root.entangledLanes |= spawnedLane;
root.entanglements[spawnedLaneIndex] |=
DeferredLane |
// If the parent render task suspended, we must also entangle those lanes
// with the spawned task.
entangledLanes;
}

export function markRootEntangled(root: FiberRoot, entangledLanes: Lanes) {
Expand Down
Loading

0 comments on commit 508437f

Please sign in to comment.