Skip to content

Commit d5e8f79

Browse files
authored
[Fiber] Use hydration lanes when scheduling hydration work (#31751)
When scheduling the initial root and when using `unstable_scheduleHydration` we should use the Hydration Lanes rather than the raw update lane. This ensures that we're always hydrating using a Hydration Lane or the Offscreen Lane rather than other lanes getting some random hydration in it. This fixes an issue where updating a root while it is still hydrating causes it to trigger client rendering when it could just hydrate and then apply the update on top of that. It also fixes a potential performance issue where `unstable_scheduleHydration` gets batched with an update that then ends up forcing an update of a boundary that requires it to rewind to do the hydration lane anyway. Might as well just start with the hydration without the update applied first. I added a kill switch (`enableHydrationLaneScheduling`) just in case but seems very safe given that using `unstable_scheduleHydration` at all is very rare and updating the root before the shell hydrates is extremely rare (and used to trigger a recoverable error).
1 parent 7130d0c commit d5e8f79

10 files changed

+110
-51
lines changed

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,39 @@ describe('ReactDOMFizzShellHydration', () => {
255255
},
256256
);
257257

258+
// @gate enableHydrationLaneScheduling
259+
it(
260+
'updating the root at same priority as initial hydration does not ' +
261+
'force a client render',
262+
async () => {
263+
function App() {
264+
return <Text text="Initial" />;
265+
}
266+
267+
// Server render
268+
await resolveText('Initial');
269+
await serverAct(async () => {
270+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
271+
pipe(writable);
272+
});
273+
assertLog(['Initial']);
274+
275+
await clientAct(async () => {
276+
let root;
277+
startTransition(() => {
278+
root = ReactDOMClient.hydrateRoot(container, <App />);
279+
});
280+
// This has lower priority than the initial hydration, so the update
281+
// won't be processed until after hydration finishes.
282+
startTransition(() => {
283+
root.render(<Text text="Updated" />);
284+
});
285+
});
286+
assertLog(['Initial', 'Updated']);
287+
expect(container.textContent).toBe('Updated');
288+
},
289+
);
290+
258291
it('updating the root while the shell is suspended forces a client render', async () => {
259292
function App() {
260293
return <AsyncText text="Shell" />;

packages/react-reconciler/src/ReactFiberLane.js

Lines changed: 53 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -995,61 +995,66 @@ export function getBumpedLaneForHydration(
995995
renderLanes: Lanes,
996996
): Lane {
997997
const renderLane = getHighestPriorityLane(renderLanes);
998-
999-
let lane;
1000-
if ((renderLane & SyncUpdateLanes) !== NoLane) {
1001-
lane = SyncHydrationLane;
1002-
} else {
1003-
switch (renderLane) {
1004-
case SyncLane:
1005-
lane = SyncHydrationLane;
1006-
break;
1007-
case InputContinuousLane:
1008-
lane = InputContinuousHydrationLane;
1009-
break;
1010-
case DefaultLane:
1011-
lane = DefaultHydrationLane;
1012-
break;
1013-
case TransitionLane1:
1014-
case TransitionLane2:
1015-
case TransitionLane3:
1016-
case TransitionLane4:
1017-
case TransitionLane5:
1018-
case TransitionLane6:
1019-
case TransitionLane7:
1020-
case TransitionLane8:
1021-
case TransitionLane9:
1022-
case TransitionLane10:
1023-
case TransitionLane11:
1024-
case TransitionLane12:
1025-
case TransitionLane13:
1026-
case TransitionLane14:
1027-
case TransitionLane15:
1028-
case RetryLane1:
1029-
case RetryLane2:
1030-
case RetryLane3:
1031-
case RetryLane4:
1032-
lane = TransitionHydrationLane;
1033-
break;
1034-
case IdleLane:
1035-
lane = IdleHydrationLane;
1036-
break;
1037-
default:
1038-
// Everything else is already either a hydration lane, or shouldn't
1039-
// be retried at a hydration lane.
1040-
lane = NoLane;
1041-
break;
1042-
}
1043-
}
1044-
998+
const bumpedLane =
999+
(renderLane & SyncUpdateLanes) !== NoLane
1000+
? // Unify sync lanes. We don't do this inside getBumpedLaneForHydrationByLane
1001+
// because that causes things to flush synchronously when they shouldn't.
1002+
// TODO: This is not coherent but that's beacuse the unification is not coherent.
1003+
// We need to get merge these into an actual single lane.
1004+
SyncHydrationLane
1005+
: getBumpedLaneForHydrationByLane(renderLane);
10451006
// Check if the lane we chose is suspended. If so, that indicates that we
10461007
// already attempted and failed to hydrate at that level. Also check if we're
10471008
// already rendering that lane, which is rare but could happen.
1048-
if ((lane & (root.suspendedLanes | renderLanes)) !== NoLane) {
1009+
// TODO: This should move into the caller to decide whether giving up is valid.
1010+
if ((bumpedLane & (root.suspendedLanes | renderLanes)) !== NoLane) {
10491011
// Give up trying to hydrate and fall back to client render.
10501012
return NoLane;
10511013
}
1014+
return bumpedLane;
1015+
}
10521016

1017+
export function getBumpedLaneForHydrationByLane(lane: Lane): Lane {
1018+
switch (lane) {
1019+
case SyncLane:
1020+
lane = SyncHydrationLane;
1021+
break;
1022+
case InputContinuousLane:
1023+
lane = InputContinuousHydrationLane;
1024+
break;
1025+
case DefaultLane:
1026+
lane = DefaultHydrationLane;
1027+
break;
1028+
case TransitionLane1:
1029+
case TransitionLane2:
1030+
case TransitionLane3:
1031+
case TransitionLane4:
1032+
case TransitionLane5:
1033+
case TransitionLane6:
1034+
case TransitionLane7:
1035+
case TransitionLane8:
1036+
case TransitionLane9:
1037+
case TransitionLane10:
1038+
case TransitionLane11:
1039+
case TransitionLane12:
1040+
case TransitionLane13:
1041+
case TransitionLane14:
1042+
case TransitionLane15:
1043+
case RetryLane1:
1044+
case RetryLane2:
1045+
case RetryLane3:
1046+
case RetryLane4:
1047+
lane = TransitionHydrationLane;
1048+
break;
1049+
case IdleLane:
1050+
lane = IdleHydrationLane;
1051+
break;
1052+
default:
1053+
// Everything else is already either a hydration lane, or shouldn't
1054+
// be retried at a hydration lane.
1055+
lane = NoLane;
1056+
break;
1057+
}
10531058
return lane;
10541059
}
10551060

packages/react-reconciler/src/ReactFiberReconciler.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ import {
3838
} from './ReactWorkTags';
3939
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
4040
import isArray from 'shared/isArray';
41-
import {enableSchedulingProfiler} from 'shared/ReactFeatureFlags';
41+
import {
42+
enableSchedulingProfiler,
43+
enableHydrationLaneScheduling,
44+
} from 'shared/ReactFeatureFlags';
4245
import ReactSharedInternals from 'shared/ReactSharedInternals';
4346
import {
4447
getPublicInstance,
@@ -91,6 +94,7 @@ import {
9194
SelectiveHydrationLane,
9295
getHighestPriorityPendingLanes,
9396
higherPriorityLane,
97+
getBumpedLaneForHydrationByLane,
9498
} from './ReactFiberLane';
9599
import {
96100
scheduleRefresh,
@@ -322,7 +326,10 @@ export function createHydrationContainer(
322326
// the update to schedule work on the root fiber (and, for legacy roots, to
323327
// enqueue the callback if one is provided).
324328
const current = root.current;
325-
const lane = requestUpdateLane(current);
329+
let lane = requestUpdateLane(current);
330+
if (enableHydrationLaneScheduling) {
331+
lane = getBumpedLaneForHydrationByLane(lane);
332+
}
326333
const update = createUpdate(lane);
327334
update.callback =
328335
callback !== undefined && callback !== null ? callback : null;
@@ -533,7 +540,10 @@ export function attemptHydrationAtCurrentPriority(fiber: Fiber): void {
533540
// their priority other than synchronously flush it.
534541
return;
535542
}
536-
const lane = requestUpdateLane(fiber);
543+
let lane = requestUpdateLane(fiber);
544+
if (enableHydrationLaneScheduling) {
545+
lane = getBumpedLaneForHydrationByLane(lane);
546+
}
537547
const root = enqueueConcurrentRenderForLane(fiber, lane);
538548
if (root !== null) {
539549
scheduleUpdateOnFiber(root, fiber, lane);

packages/shared/ReactFeatureFlags.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ export const enableSuspenseAvoidThisFallbackFizz = false;
116116

117117
export const enableCPUSuspense = __EXPERIMENTAL__;
118118

119+
export const enableHydrationLaneScheduling = true;
120+
119121
// Enables useMemoCache hook, intended as a compilation target for
120122
// auto-memoization.
121123
export const enableUseMemoCacheHook = true;

packages/shared/forks/ReactFeatureFlags.native-fb.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export const retryLaneExpirationMs = 5000;
9494
export const syncLaneExpirationMs = 250;
9595
export const transitionLaneExpirationMs = 5000;
9696
export const useModernStrictMode = true;
97+
export const enableHydrationLaneScheduling = true;
9798

9899
// Flow magic to verify the exports of this file match the original version.
99100
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

packages/shared/forks/ReactFeatureFlags.native-oss.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ export const useModernStrictMode = true;
8787
export const enableSiblingPrerendering = true;
8888
export const enableUseResourceEffectHook = false;
8989

90+
export const enableHydrationLaneScheduling = true;
91+
9092
// Profiling Only
9193
export const enableProfilerTimer = __PROFILE__;
9294
export const enableProfilerCommitHooks = __PROFILE__;

packages/shared/forks/ReactFeatureFlags.test-renderer.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const enableMoveBefore = false;
4949
export const enableGetInspectorDataForInstanceInProduction = false;
5050
export const enableFabricCompleteRootInCommitPhase = false;
5151
export const enableHiddenSubtreeInsertionEffectCleanup = false;
52+
export const enableHydrationLaneScheduling = true;
5253

5354
export const enableRetryLaneExpiration = false;
5455
export const retryLaneExpirationMs = 5000;

packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export const useModernStrictMode = true;
8282
export const enableFabricCompleteRootInCommitPhase = false;
8383
export const enableSiblingPrerendering = true;
8484
export const enableUseResourceEffectHook = true;
85+
export const enableHydrationLaneScheduling = true;
8586

8687
// Flow magic to verify the exports of this file match the original version.
8788
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

packages/shared/forks/ReactFeatureFlags.test-renderer.www.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,5 +97,7 @@ export const enableSiblingPrerendering = true;
9797

9898
export const enableUseResourceEffectHook = false;
9999

100+
export const enableHydrationLaneScheduling = true;
101+
100102
// Flow magic to verify the exports of this file match the original version.
101103
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

packages/shared/forks/ReactFeatureFlags.www.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ export const disableInputAttributeSyncing = false;
6464
export const enableLegacyFBSupport = true;
6565
export const enableLazyContextPropagation = true;
6666

67+
export const enableHydrationLaneScheduling = true;
68+
6769
export const enableComponentPerformanceTrack = false;
6870

6971
// Logs additional User Timing API marks for use with an experimental profiling tool.

0 commit comments

Comments
 (0)