mirror of
https://github.com/facebook/react.git
synced 2026-02-23 20:23:02 +00:00
[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).
This commit is contained in:
committed by
GitHub
parent
7130d0c6cf
commit
d5e8f79cf4
@@ -255,6 +255,39 @@ describe('ReactDOMFizzShellHydration', () => {
|
||||
},
|
||||
);
|
||||
|
||||
// @gate enableHydrationLaneScheduling
|
||||
it(
|
||||
'updating the root at same priority as initial hydration does not ' +
|
||||
'force a client render',
|
||||
async () => {
|
||||
function App() {
|
||||
return <Text text="Initial" />;
|
||||
}
|
||||
|
||||
// Server render
|
||||
await resolveText('Initial');
|
||||
await serverAct(async () => {
|
||||
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
|
||||
pipe(writable);
|
||||
});
|
||||
assertLog(['Initial']);
|
||||
|
||||
await clientAct(async () => {
|
||||
let root;
|
||||
startTransition(() => {
|
||||
root = ReactDOMClient.hydrateRoot(container, <App />);
|
||||
});
|
||||
// This has lower priority than the initial hydration, so the update
|
||||
// won't be processed until after hydration finishes.
|
||||
startTransition(() => {
|
||||
root.render(<Text text="Updated" />);
|
||||
});
|
||||
});
|
||||
assertLog(['Initial', 'Updated']);
|
||||
expect(container.textContent).toBe('Updated');
|
||||
},
|
||||
);
|
||||
|
||||
it('updating the root while the shell is suspended forces a client render', async () => {
|
||||
function App() {
|
||||
return <AsyncText text="Shell" />;
|
||||
|
||||
101
packages/react-reconciler/src/ReactFiberLane.js
vendored
101
packages/react-reconciler/src/ReactFiberLane.js
vendored
@@ -995,61 +995,66 @@ export function getBumpedLaneForHydration(
|
||||
renderLanes: Lanes,
|
||||
): Lane {
|
||||
const renderLane = getHighestPriorityLane(renderLanes);
|
||||
|
||||
let lane;
|
||||
if ((renderLane & SyncUpdateLanes) !== NoLane) {
|
||||
lane = SyncHydrationLane;
|
||||
} else {
|
||||
switch (renderLane) {
|
||||
case SyncLane:
|
||||
lane = SyncHydrationLane;
|
||||
break;
|
||||
case InputContinuousLane:
|
||||
lane = InputContinuousHydrationLane;
|
||||
break;
|
||||
case DefaultLane:
|
||||
lane = DefaultHydrationLane;
|
||||
break;
|
||||
case TransitionLane1:
|
||||
case TransitionLane2:
|
||||
case TransitionLane3:
|
||||
case TransitionLane4:
|
||||
case TransitionLane5:
|
||||
case TransitionLane6:
|
||||
case TransitionLane7:
|
||||
case TransitionLane8:
|
||||
case TransitionLane9:
|
||||
case TransitionLane10:
|
||||
case TransitionLane11:
|
||||
case TransitionLane12:
|
||||
case TransitionLane13:
|
||||
case TransitionLane14:
|
||||
case TransitionLane15:
|
||||
case RetryLane1:
|
||||
case RetryLane2:
|
||||
case RetryLane3:
|
||||
case RetryLane4:
|
||||
lane = TransitionHydrationLane;
|
||||
break;
|
||||
case IdleLane:
|
||||
lane = IdleHydrationLane;
|
||||
break;
|
||||
default:
|
||||
// Everything else is already either a hydration lane, or shouldn't
|
||||
// be retried at a hydration lane.
|
||||
lane = NoLane;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const bumpedLane =
|
||||
(renderLane & SyncUpdateLanes) !== NoLane
|
||||
? // Unify sync lanes. We don't do this inside getBumpedLaneForHydrationByLane
|
||||
// because that causes things to flush synchronously when they shouldn't.
|
||||
// TODO: This is not coherent but that's beacuse the unification is not coherent.
|
||||
// We need to get merge these into an actual single lane.
|
||||
SyncHydrationLane
|
||||
: getBumpedLaneForHydrationByLane(renderLane);
|
||||
// Check if the lane we chose is suspended. If so, that indicates that we
|
||||
// already attempted and failed to hydrate at that level. Also check if we're
|
||||
// already rendering that lane, which is rare but could happen.
|
||||
if ((lane & (root.suspendedLanes | renderLanes)) !== NoLane) {
|
||||
// TODO: This should move into the caller to decide whether giving up is valid.
|
||||
if ((bumpedLane & (root.suspendedLanes | renderLanes)) !== NoLane) {
|
||||
// Give up trying to hydrate and fall back to client render.
|
||||
return NoLane;
|
||||
}
|
||||
return bumpedLane;
|
||||
}
|
||||
|
||||
export function getBumpedLaneForHydrationByLane(lane: Lane): Lane {
|
||||
switch (lane) {
|
||||
case SyncLane:
|
||||
lane = SyncHydrationLane;
|
||||
break;
|
||||
case InputContinuousLane:
|
||||
lane = InputContinuousHydrationLane;
|
||||
break;
|
||||
case DefaultLane:
|
||||
lane = DefaultHydrationLane;
|
||||
break;
|
||||
case TransitionLane1:
|
||||
case TransitionLane2:
|
||||
case TransitionLane3:
|
||||
case TransitionLane4:
|
||||
case TransitionLane5:
|
||||
case TransitionLane6:
|
||||
case TransitionLane7:
|
||||
case TransitionLane8:
|
||||
case TransitionLane9:
|
||||
case TransitionLane10:
|
||||
case TransitionLane11:
|
||||
case TransitionLane12:
|
||||
case TransitionLane13:
|
||||
case TransitionLane14:
|
||||
case TransitionLane15:
|
||||
case RetryLane1:
|
||||
case RetryLane2:
|
||||
case RetryLane3:
|
||||
case RetryLane4:
|
||||
lane = TransitionHydrationLane;
|
||||
break;
|
||||
case IdleLane:
|
||||
lane = IdleHydrationLane;
|
||||
break;
|
||||
default:
|
||||
// Everything else is already either a hydration lane, or shouldn't
|
||||
// be retried at a hydration lane.
|
||||
lane = NoLane;
|
||||
break;
|
||||
}
|
||||
return lane;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,10 @@ import {
|
||||
} from './ReactWorkTags';
|
||||
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
|
||||
import isArray from 'shared/isArray';
|
||||
import {enableSchedulingProfiler} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
enableSchedulingProfiler,
|
||||
enableHydrationLaneScheduling,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
import {
|
||||
getPublicInstance,
|
||||
@@ -91,6 +94,7 @@ import {
|
||||
SelectiveHydrationLane,
|
||||
getHighestPriorityPendingLanes,
|
||||
higherPriorityLane,
|
||||
getBumpedLaneForHydrationByLane,
|
||||
} from './ReactFiberLane';
|
||||
import {
|
||||
scheduleRefresh,
|
||||
@@ -322,7 +326,10 @@ export function createHydrationContainer(
|
||||
// the update to schedule work on the root fiber (and, for legacy roots, to
|
||||
// enqueue the callback if one is provided).
|
||||
const current = root.current;
|
||||
const lane = requestUpdateLane(current);
|
||||
let lane = requestUpdateLane(current);
|
||||
if (enableHydrationLaneScheduling) {
|
||||
lane = getBumpedLaneForHydrationByLane(lane);
|
||||
}
|
||||
const update = createUpdate(lane);
|
||||
update.callback =
|
||||
callback !== undefined && callback !== null ? callback : null;
|
||||
@@ -533,7 +540,10 @@ export function attemptHydrationAtCurrentPriority(fiber: Fiber): void {
|
||||
// their priority other than synchronously flush it.
|
||||
return;
|
||||
}
|
||||
const lane = requestUpdateLane(fiber);
|
||||
let lane = requestUpdateLane(fiber);
|
||||
if (enableHydrationLaneScheduling) {
|
||||
lane = getBumpedLaneForHydrationByLane(lane);
|
||||
}
|
||||
const root = enqueueConcurrentRenderForLane(fiber, lane);
|
||||
if (root !== null) {
|
||||
scheduleUpdateOnFiber(root, fiber, lane);
|
||||
|
||||
@@ -116,6 +116,8 @@ export const enableSuspenseAvoidThisFallbackFizz = false;
|
||||
|
||||
export const enableCPUSuspense = __EXPERIMENTAL__;
|
||||
|
||||
export const enableHydrationLaneScheduling = true;
|
||||
|
||||
// Enables useMemoCache hook, intended as a compilation target for
|
||||
// auto-memoization.
|
||||
export const enableUseMemoCacheHook = true;
|
||||
|
||||
@@ -94,6 +94,7 @@ export const retryLaneExpirationMs = 5000;
|
||||
export const syncLaneExpirationMs = 250;
|
||||
export const transitionLaneExpirationMs = 5000;
|
||||
export const useModernStrictMode = true;
|
||||
export const enableHydrationLaneScheduling = true;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
||||
|
||||
@@ -87,6 +87,8 @@ export const useModernStrictMode = true;
|
||||
export const enableSiblingPrerendering = true;
|
||||
export const enableUseResourceEffectHook = false;
|
||||
|
||||
export const enableHydrationLaneScheduling = true;
|
||||
|
||||
// Profiling Only
|
||||
export const enableProfilerTimer = __PROFILE__;
|
||||
export const enableProfilerCommitHooks = __PROFILE__;
|
||||
|
||||
@@ -49,6 +49,7 @@ export const enableMoveBefore = false;
|
||||
export const enableGetInspectorDataForInstanceInProduction = false;
|
||||
export const enableFabricCompleteRootInCommitPhase = false;
|
||||
export const enableHiddenSubtreeInsertionEffectCleanup = false;
|
||||
export const enableHydrationLaneScheduling = true;
|
||||
|
||||
export const enableRetryLaneExpiration = false;
|
||||
export const retryLaneExpirationMs = 5000;
|
||||
|
||||
@@ -82,6 +82,7 @@ export const useModernStrictMode = true;
|
||||
export const enableFabricCompleteRootInCommitPhase = false;
|
||||
export const enableSiblingPrerendering = true;
|
||||
export const enableUseResourceEffectHook = true;
|
||||
export const enableHydrationLaneScheduling = true;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
||||
|
||||
@@ -97,5 +97,7 @@ export const enableSiblingPrerendering = true;
|
||||
|
||||
export const enableUseResourceEffectHook = false;
|
||||
|
||||
export const enableHydrationLaneScheduling = true;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
||||
|
||||
@@ -64,6 +64,8 @@ export const disableInputAttributeSyncing = false;
|
||||
export const enableLegacyFBSupport = true;
|
||||
export const enableLazyContextPropagation = true;
|
||||
|
||||
export const enableHydrationLaneScheduling = true;
|
||||
|
||||
export const enableComponentPerformanceTrack = false;
|
||||
|
||||
// Logs additional User Timing API marks for use with an experimental profiling tool.
|
||||
|
||||
Reference in New Issue
Block a user