Skip to content

Commit 89e8b89

Browse files
committed
useDeferredValue switches to final if initial suspends
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.
1 parent 61c2fbb commit 89e8b89

File tree

7 files changed

+438
-66
lines changed

7 files changed

+438
-66
lines changed

packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js

Lines changed: 26 additions & 26 deletions
Large diffs are not rendered by default.

packages/react-devtools-shared/src/__tests__/preprocessData-test.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2001,15 +2001,15 @@ describe('Timeline profiler', () => {
20012001
524288 => "Transition",
20022002
1048576 => "Transition",
20032003
2097152 => "Transition",
2004-
4194304 => "Transition",
2004+
4194304 => "Retry",
20052005
8388608 => "Retry",
20062006
16777216 => "Retry",
20072007
33554432 => "Retry",
2008-
67108864 => "Retry",
2009-
134217728 => "SelectiveHydration",
2010-
268435456 => "IdleHydration",
2011-
536870912 => "Idle",
2012-
1073741824 => "Offscreen",
2008+
67108864 => "SelectiveHydration",
2009+
134217728 => "IdleHydration",
2010+
268435456 => "Idle",
2011+
536870912 => "Offscreen",
2012+
1073741824 => "Deferred",
20132013
},
20142014
"laneToReactMeasureMap": Map {
20152015
1 => [],
@@ -2269,15 +2269,15 @@ describe('Timeline profiler', () => {
22692269
524288 => "Transition",
22702270
1048576 => "Transition",
22712271
2097152 => "Transition",
2272-
4194304 => "Transition",
2272+
4194304 => "Retry",
22732273
8388608 => "Retry",
22742274
16777216 => "Retry",
22752275
33554432 => "Retry",
2276-
67108864 => "Retry",
2277-
134217728 => "SelectiveHydration",
2278-
268435456 => "IdleHydration",
2279-
536870912 => "Idle",
2280-
1073741824 => "Offscreen",
2276+
67108864 => "SelectiveHydration",
2277+
134217728 => "IdleHydration",
2278+
268435456 => "Idle",
2279+
536870912 => "Offscreen",
2280+
1073741824 => "Deferred",
22812281
},
22822282
"laneToReactMeasureMap": Map {
22832283
1 => [],

packages/react-reconciler/src/ReactFiberHooks.js

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,17 @@ import {
6161
NoLane,
6262
SyncLane,
6363
OffscreenLane,
64+
DeferredLane,
6465
NoLanes,
6566
isSubsetOfLanes,
6667
includesBlockingLane,
6768
includesOnlyNonUrgentLanes,
68-
claimNextTransitionLane,
6969
mergeLanes,
7070
removeLanes,
7171
intersectLanes,
7272
isTransitionLane,
7373
markRootEntangled,
74+
includesSomeLane,
7475
} from './ReactFiberLane';
7576
import {
7677
ContinuousEventPriority,
@@ -101,6 +102,7 @@ import {
101102
getWorkInProgressRootRenderLanes,
102103
scheduleUpdateOnFiber,
103104
requestUpdateLane,
105+
requestDeferredLane,
104106
markSkippedUpdateLanes,
105107
isInvalidExecutionContextForEventFunction,
106108
} from './ReactFiberWorkLoop';
@@ -2665,16 +2667,21 @@ function rerenderDeferredValue<T>(value: T, initialValue?: T): T {
26652667
}
26662668

26672669
function mountDeferredValueImpl<T>(hook: Hook, value: T, initialValue?: T): T {
2668-
if (enableUseDeferredValueInitialArg && initialValue !== undefined) {
2670+
if (
2671+
enableUseDeferredValueInitialArg &&
26692672
// When `initialValue` is provided, we defer the initial render even if the
26702673
// current render is not synchronous.
2671-
// TODO: However, to avoid waterfalls, we should not defer if this render
2672-
// was itself spawned by an earlier useDeferredValue. Plan is to add a
2673-
// Deferred lane to track this.
2674+
initialValue !== undefined &&
2675+
// However, to avoid waterfalls, we do not defer if this render
2676+
// was itself spawned by an earlier useDeferredValue. Check if DeferredLane
2677+
// is part of the render lanes.
2678+
!includesSomeLane(renderLanes, DeferredLane)
2679+
) {
2680+
// Render with the initial value
26742681
hook.memoizedState = initialValue;
26752682

2676-
// Schedule a deferred render
2677-
const deferredLane = claimNextTransitionLane();
2683+
// Schedule a deferred render to switch to the final value.
2684+
const deferredLane = requestDeferredLane();
26782685
currentlyRenderingFiber.lanes = mergeLanes(
26792686
currentlyRenderingFiber.lanes,
26802687
deferredLane,
@@ -2710,7 +2717,7 @@ function updateDeferredValueImpl<T>(
27102717

27112718
if (!is(value, prevValue)) {
27122719
// Schedule a deferred render
2713-
const deferredLane = claimNextTransitionLane();
2720+
const deferredLane = requestDeferredLane();
27142721
currentlyRenderingFiber.lanes = mergeLanes(
27152722
currentlyRenderingFiber.lanes,
27162723
deferredLane,

packages/react-reconciler/src/ReactFiberLane.js

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -622,7 +622,11 @@ export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
622622
}
623623
}
624624

625-
export function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {
625+
export function markRootSuspended(
626+
root: FiberRoot,
627+
suspendedLanes: Lanes,
628+
spawnedLane: Lane,
629+
) {
626630
root.suspendedLanes |= suspendedLanes;
627631
root.pingedLanes &= ~suspendedLanes;
628632

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

638642
lanes &= ~lane;
639643
}
644+
645+
if (spawnedLane !== NoLane) {
646+
markSpawnedDeferredLane(root, spawnedLane, suspendedLanes);
647+
}
640648
}
641649

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

646-
export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
654+
export function markRootFinished(
655+
root: FiberRoot,
656+
remainingLanes: Lanes,
657+
spawnedLane: Lane,
658+
) {
647659
const noLongerPendingLanes = root.pendingLanes & ~remainingLanes;
648660

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

690702
lanes &= ~lane;
691703
}
704+
705+
if (spawnedLane !== NoLane) {
706+
markSpawnedDeferredLane(
707+
root,
708+
spawnedLane,
709+
// This render finished successfully without suspending, so we don't need
710+
// to entangle the spawned task with the parent task.
711+
NoLanes,
712+
);
713+
}
714+
}
715+
716+
function markSpawnedDeferredLane(
717+
root: FiberRoot,
718+
spawnedLane: Lane,
719+
entangledLanes: Lanes,
720+
) {
721+
// This render spawned a deferred task. Mark it as pending.
722+
root.pendingLanes |= spawnedLane;
723+
root.suspendedLanes &= ~spawnedLane;
724+
725+
// Entangle the spawned lane with the DeferredLane bit so that we know it
726+
// was the result of another render. This lets us avoid a useDeferredValue
727+
// waterfall — only the first level will defer.
728+
const spawnedLaneIndex = laneToIndex(spawnedLane);
729+
root.entangledLanes |= spawnedLane;
730+
root.entanglements[spawnedLaneIndex] |=
731+
DeferredLane |
732+
// If the parent render task suspended, we must also entangle those lanes
733+
// with the spawned task.
734+
entangledLanes;
692735
}
693736

694737
export function markRootEntangled(root: FiberRoot, entangledLanes: Lanes) {

0 commit comments

Comments
 (0)