@samitouri / QOS-React-2 / commits / d5e8f79cf4

[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).

Sebastian Markbåge committed Dec 12, 2024 at 23:06 UTC d5e8f79cf4d11fa7eee263b3f937deecbe65ffd7
10 files changed +110 -51
packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js
+33
@@ -255,6 +255,39 @@ describe('ReactDOMFizzShellHydration', () => {
255 },
256 );
257
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 +
291 it('updating the root while the shell is suspended forces a client render', async () => {
292 function App() {
293 return <AsyncText text="Shell" />;
packages/react-reconciler/src/ReactFiberLane.js
+53 -48
@@ -995,61 +995,66 @@ export function getBumpedLaneForHydration(
995 renderLanes: Lanes,
996 ): Lane {
997 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);
1006 // Check if the lane we chose is suspended. If so, that indicates that we
1007 // already attempted and failed to hydrate at that level. Also check if we're
1008 // 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) {
1011 // Give up trying to hydrate and fall back to client render.
1012 return NoLane;
1013 }
1014 + return bumpedLane;
1015 +}
1016
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 + }
1058 return lane;
1059 }
1060
packages/react-reconciler/src/ReactFiberReconciler.js
+13 -3
@@ -38,7 +38,10 @@ import {
38 } from './ReactWorkTags';
39 import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
40 import isArray from 'shared/isArray';
41 -import {enableSchedulingProfiler} from 'shared/ReactFeatureFlags';
41 +import {
42 + enableSchedulingProfiler,
43 + enableHydrationLaneScheduling,
44 +} from 'shared/ReactFeatureFlags';
45 import ReactSharedInternals from 'shared/ReactSharedInternals';
46 import {
47 getPublicInstance,
@@ -91,6 +94,7 @@ import {
94 SelectiveHydrationLane,
95 getHighestPriorityPendingLanes,
96 higherPriorityLane,
97 + getBumpedLaneForHydrationByLane,
98 } from './ReactFiberLane';
99 import {
100 scheduleRefresh,
@@ -322,7 +326,10 @@ export function createHydrationContainer(
326 // the update to schedule work on the root fiber (and, for legacy roots, to
327 // enqueue the callback if one is provided).
328 const current = root.current;
325 - const lane = requestUpdateLane(current);
329 + let lane = requestUpdateLane(current);
330 + if (enableHydrationLaneScheduling) {
331 + lane = getBumpedLaneForHydrationByLane(lane);
332 + }
333 const update = createUpdate(lane);
334 update.callback =
335 callback !== undefined && callback !== null ? callback : null;
@@ -533,7 +540,10 @@ export function attemptHydrationAtCurrentPriority(fiber: Fiber): void {
540 // their priority other than synchronously flush it.
541 return;
542 }
536 - const lane = requestUpdateLane(fiber);
543 + let lane = requestUpdateLane(fiber);
544 + if (enableHydrationLaneScheduling) {
545 + lane = getBumpedLaneForHydrationByLane(lane);
546 + }
547 const root = enqueueConcurrentRenderForLane(fiber, lane);
548 if (root !== null) {
549 scheduleUpdateOnFiber(root, fiber, lane);
packages/shared/ReactFeatureFlags.js
+2
@@ -116,6 +116,8 @@ export const enableSuspenseAvoidThisFallbackFizz = false;
116
117 export const enableCPUSuspense = __EXPERIMENTAL__;
118
119 +export const enableHydrationLaneScheduling = true;
120 +
121 // Enables useMemoCache hook, intended as a compilation target for
122 // auto-memoization.
123 export const enableUseMemoCacheHook = true;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -94,6 +94,7 @@ export const retryLaneExpirationMs = 5000;
94 export const syncLaneExpirationMs = 250;
95 export const transitionLaneExpirationMs = 5000;
96 export const useModernStrictMode = true;
97 +export const enableHydrationLaneScheduling = true;
98
99 // Flow magic to verify the exports of this file match the original version.
100 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.native-oss.js
+2
@@ -87,6 +87,8 @@ export const useModernStrictMode = true;
87 export const enableSiblingPrerendering = true;
88 export const enableUseResourceEffectHook = false;
89
90 +export const enableHydrationLaneScheduling = true;
91 +
92 // Profiling Only
93 export const enableProfilerTimer = __PROFILE__;
94 export const enableProfilerCommitHooks = __PROFILE__;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -49,6 +49,7 @@ export const enableMoveBefore = false;
49 export const enableGetInspectorDataForInstanceInProduction = false;
50 export const enableFabricCompleteRootInCommitPhase = false;
51 export const enableHiddenSubtreeInsertionEffectCleanup = false;
52 +export const enableHydrationLaneScheduling = true;
53
54 export const enableRetryLaneExpiration = false;
55 export const retryLaneExpirationMs = 5000;
packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
+1
@@ -82,6 +82,7 @@ export const useModernStrictMode = true;
82 export const enableFabricCompleteRootInCommitPhase = false;
83 export const enableSiblingPrerendering = true;
84 export const enableUseResourceEffectHook = true;
85 +export const enableHydrationLaneScheduling = true;
86
87 // Flow magic to verify the exports of this file match the original version.
88 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+2
@@ -97,5 +97,7 @@ export const enableSiblingPrerendering = true;
97
98 export const enableUseResourceEffectHook = false;
99
100 +export const enableHydrationLaneScheduling = true;
101 +
102 // Flow magic to verify the exports of this file match the original version.
103 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -64,6 +64,8 @@ export const disableInputAttributeSyncing = false;
64 export const enableLegacyFBSupport = true;
65 export const enableLazyContextPropagation = true;
66
67 +export const enableHydrationLaneScheduling = true;
68 +
69 export const enableComponentPerformanceTrack = false;
70
71 // Logs additional User Timing API marks for use with an experimental profiling tool.