Skip to content

Commit

Permalink
useDeferredValue should skip initialValue if it suspends (#27509)
Browse files Browse the repository at this point in the history
### Based on #27505

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 authored Oct 17, 2023
1 parent 9abf6fa commit b2a68a6
Show file tree
Hide file tree
Showing 7 changed files with 458 additions and 81 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
82 changes: 65 additions & 17 deletions packages/react-reconciler/src/ReactFiberLane.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const SyncUpdateLanes: Lane = enableUnifiedSyncLane
: SyncLane;

const TransitionHydrationLane: Lane = /* */ 0b0000000000000000000000001000000;
const TransitionLanes: Lanes = /* */ 0b0000000011111111111111110000000;
const TransitionLanes: Lanes = /* */ 0b0000000001111111111111110000000;
const TransitionLane1: Lane = /* */ 0b0000000000000000000000010000000;
const TransitionLane2: Lane = /* */ 0b0000000000000000000000100000000;
const TransitionLane3: Lane = /* */ 0b0000000000000000000001000000000;
Expand All @@ -68,24 +68,24 @@ const TransitionLane12: Lane = /* */ 0b0000000000001000000
const TransitionLane13: Lane = /* */ 0b0000000000010000000000000000000;
const TransitionLane14: Lane = /* */ 0b0000000000100000000000000000000;
const TransitionLane15: Lane = /* */ 0b0000000001000000000000000000000;
const TransitionLane16: Lane = /* */ 0b0000000010000000000000000000000;

const RetryLanes: Lanes = /* */ 0b0000111100000000000000000000000;
const RetryLane1: Lane = /* */ 0b0000000100000000000000000000000;
const RetryLane2: Lane = /* */ 0b0000001000000000000000000000000;
const RetryLane3: Lane = /* */ 0b0000010000000000000000000000000;
const RetryLane4: Lane = /* */ 0b0000100000000000000000000000000;
const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;
const RetryLane1: Lane = /* */ 0b0000000010000000000000000000000;
const RetryLane2: Lane = /* */ 0b0000000100000000000000000000000;
const RetryLane3: Lane = /* */ 0b0000001000000000000000000000000;
const RetryLane4: Lane = /* */ 0b0000010000000000000000000000000;

export const SomeRetryLane: Lane = RetryLane1;

export const SelectiveHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;
export const SelectiveHydrationLane: Lane = /* */ 0b0000100000000000000000000000000;

const NonIdleLanes: Lanes = /* */ 0b0001111111111111111111111111111;
const NonIdleLanes: Lanes = /* */ 0b0000111111111111111111111111111;

export const IdleHydrationLane: Lane = /* */ 0b0010000000000000000000000000000;
export const IdleLane: Lane = /* */ 0b0100000000000000000000000000000;
export const IdleHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;
export const IdleLane: Lane = /* */ 0b0010000000000000000000000000000;

export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000;
export const OffscreenLane: Lane = /* */ 0b0100000000000000000000000000000;
export const DeferredLane: Lane = /* */ 0b1000000000000000000000000000000;

// Any lane that might schedule an update. This is used to detect infinite
// update loops, so it doesn't include hydration lanes or retries.
Expand Down Expand Up @@ -135,6 +135,9 @@ export function getLabelForLane(lane: Lane): string | void {
if (lane & OffscreenLane) {
return 'Offscreen';
}
if (lane & DeferredLane) {
return 'Deferred';
}
}
}

Expand Down Expand Up @@ -180,7 +183,6 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
case TransitionLane13:
case TransitionLane14:
case TransitionLane15:
case TransitionLane16:
return lanes & TransitionLanes;
case RetryLane1:
case RetryLane2:
Expand All @@ -195,6 +197,10 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
return IdleLane;
case OffscreenLane:
return OffscreenLane;
case DeferredLane:
// This shouldn't be reachable because deferred work is always entangled
// with something else.
return NoLanes;
default:
if (__DEV__) {
console.error(
Expand Down Expand Up @@ -367,7 +373,6 @@ function computeExpirationTime(lane: Lane, currentTime: number) {
case TransitionLane13:
case TransitionLane14:
case TransitionLane15:
case TransitionLane16:
return currentTime + 5000;
case RetryLane1:
case RetryLane2:
Expand All @@ -383,6 +388,7 @@ function computeExpirationTime(lane: Lane, currentTime: number) {
case IdleHydrationLane:
case IdleLane:
case OffscreenLane:
case DeferredLane:
// Anything idle priority or lower should never expire.
return NoTimestamp;
default:
Expand Down Expand Up @@ -616,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 @@ -631,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 @@ -683,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 Expand Up @@ -795,7 +844,6 @@ export function getBumpedLaneForHydration(
case TransitionLane13:
case TransitionLane14:
case TransitionLane15:
case TransitionLane16:
case RetryLane1:
case RetryLane2:
case RetryLane3:
Expand Down
Loading

0 comments on commit b2a68a6

Please sign in to comment.