diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 119129d506c09..0ae0e668d22ac 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -111,6 +111,7 @@ import { disableLegacyMode, disableDefaultPropsExceptForClasses, enableOwnerStacks, + enableHydrationLaneScheduling, } from 'shared/ReactFeatureFlags'; import isArray from 'shared/isArray'; import shallowEqual from 'shared/shallowEqual'; @@ -146,6 +147,7 @@ import { NoLane, NoLanes, OffscreenLane, + DefaultLane, DefaultHydrationLane, SomeRetryLane, includesSomeLane, @@ -2646,14 +2648,10 @@ function mountDehydratedSuspenseComponent( // have timed out. In theory we could render it in this pass but it would have the // wrong priority associated with it and will prevent hydration of parent path. // Instead, we'll leave work left on it to render it in a separate commit. - - // TODO This time should be the time at which the server rendered response that is - // a parent to this boundary was displayed. However, since we currently don't have - // a protocol to transfer that time, we'll just estimate it by using the current - // time. This will mean that Suspense timeouts are slightly shifted to later than - // they should be. // Schedule a normal pri update to render this content. - workInProgress.lanes = laneToLanes(DefaultHydrationLane); + workInProgress.lanes = laneToLanes( + enableHydrationLaneScheduling ? DefaultLane : DefaultHydrationLane, + ); } else { // We'll continue hydrating the rest at offscreen priority since we'll already // be showing the right content coming from the server, it is no rush. diff --git a/packages/react-reconciler/src/ReactFiberLane.js b/packages/react-reconciler/src/ReactFiberLane.js index ee3a0a3df2b60..4c96207bd6696 100644 --- a/packages/react-reconciler/src/ReactFiberLane.js +++ b/packages/react-reconciler/src/ReactFiberLane.js @@ -621,6 +621,18 @@ export function includesTransitionLane(lanes: Lanes): boolean { return (lanes & TransitionLanes) !== NoLanes; } +export function includesOnlyHydrationLanes(lanes: Lanes): boolean { + return (lanes & HydrationLanes) === lanes; +} + +export function includesOnlyOffscreenLanes(lanes: Lanes): boolean { + return (lanes & OffscreenLane) === lanes; +} + +export function includesOnlyHydrationOrOffscreenLanes(lanes: Lanes): boolean { + return (lanes & (HydrationLanes | OffscreenLane)) === lanes; +} + export function includesBlockingLane(lanes: Lanes): boolean { const SyncDefaultLanes = InputContinuousHydrationLane | diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index 56470bb0e55d6..7e9e246d8beb2 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -9,9 +9,16 @@ import type {Fiber} from './ReactInternalTypes'; +import type {Lanes} from './ReactFiberLane'; + import getComponentNameFromFiber from './getComponentNameFromFiber'; -import {getGroupNameOfHighestPriorityLane} from './ReactFiberLane'; +import { + getGroupNameOfHighestPriorityLane, + includesOnlyHydrationLanes, + includesOnlyOffscreenLanes, + includesOnlyHydrationOrOffscreenLanes, +} from './ReactFiberLane'; import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; @@ -51,7 +58,7 @@ const reusableLaneOptions = { }, }; -export function setCurrentTrackFromLanes(lanes: number): void { +export function setCurrentTrackFromLanes(lanes: Lanes): void { reusableLaneDevToolDetails.track = getGroupNameOfHighestPriorityLane(lanes); } @@ -223,6 +230,7 @@ export function logBlockingStart( eventType: null | string, eventIsRepeat: boolean, renderStartTime: number, + lanes: Lanes, ): void { if (supportsUserTiming) { reusableLaneDevToolDetails.track = 'Blocking'; @@ -240,7 +248,11 @@ export function logBlockingStart( } if (updateTime > 0) { // Log the time from when we called setState until we started rendering. - reusableLaneDevToolDetails.color = 'primary-light'; + reusableLaneDevToolDetails.color = includesOnlyHydrationOrOffscreenLanes( + lanes, + ) + ? 'tertiary-light' + : 'primary-light'; reusableLaneOptions.start = updateTime; reusableLaneOptions.end = renderStartTime; performance.measure('Blocked', reusableLaneOptions); @@ -292,33 +304,65 @@ export function logTransitionStart( } } -export function logRenderPhase(startTime: number, endTime: number): void { +export function logRenderPhase( + startTime: number, + endTime: number, + lanes: Lanes, +): void { if (supportsUserTiming) { - reusableLaneDevToolDetails.color = 'primary-dark'; + reusableLaneDevToolDetails.color = includesOnlyHydrationOrOffscreenLanes( + lanes, + ) + ? 'tertiary-dark' + : 'primary-dark'; reusableLaneOptions.start = startTime; reusableLaneOptions.end = endTime; - performance.measure('Render', reusableLaneOptions); + performance.measure( + includesOnlyOffscreenLanes(lanes) + ? 'Prepared' + : includesOnlyHydrationLanes(lanes) + ? 'Hydrated' + : 'Render', + reusableLaneOptions, + ); } } export function logInterruptedRenderPhase( startTime: number, endTime: number, + lanes: Lanes, ): void { if (supportsUserTiming) { - reusableLaneDevToolDetails.color = 'primary-dark'; + reusableLaneDevToolDetails.color = includesOnlyHydrationOrOffscreenLanes( + lanes, + ) + ? 'tertiary-dark' + : 'primary-dark'; reusableLaneOptions.start = startTime; reusableLaneOptions.end = endTime; - performance.measure('Interrupted Render', reusableLaneOptions); + performance.measure( + includesOnlyOffscreenLanes(lanes) + ? 'Prewarm' + : includesOnlyHydrationLanes(lanes) + ? 'Interrupted Hydration' + : 'Interrupted Render', + reusableLaneOptions, + ); } } export function logSuspendedRenderPhase( startTime: number, endTime: number, + lanes: Lanes, ): void { if (supportsUserTiming) { - reusableLaneDevToolDetails.color = 'primary-dark'; + reusableLaneDevToolDetails.color = includesOnlyHydrationOrOffscreenLanes( + lanes, + ) + ? 'tertiary-dark' + : 'primary-dark'; reusableLaneOptions.start = startTime; reusableLaneOptions.end = endTime; performance.measure('Prewarm', reusableLaneOptions); @@ -328,10 +372,15 @@ export function logSuspendedRenderPhase( export function logSuspendedWithDelayPhase( startTime: number, endTime: number, + lanes: Lanes, ): void { // This means the render was suspended and cannot commit until it gets unblocked. if (supportsUserTiming) { - reusableLaneDevToolDetails.color = 'primary-dark'; + reusableLaneDevToolDetails.color = includesOnlyHydrationOrOffscreenLanes( + lanes, + ) + ? 'tertiary-dark' + : 'primary-dark'; reusableLaneOptions.start = startTime; reusableLaneOptions.end = endTime; performance.measure('Suspended', reusableLaneOptions); @@ -341,6 +390,7 @@ export function logSuspendedWithDelayPhase( export function logErroredRenderPhase( startTime: number, endTime: number, + lanes: Lanes, ): void { if (supportsUserTiming) { reusableLaneDevToolDetails.color = 'error'; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index f812d16c9c65a..dd637d687da4f 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -1035,7 +1035,7 @@ export function performWorkOnRoot( if (errorRetryLanes !== NoLanes) { if (enableProfilerTimer && enableComponentPerformanceTrack) { setCurrentTrackFromLanes(lanes); - logErroredRenderPhase(renderStartTime, renderEndTime); + logErroredRenderPhase(renderStartTime, renderEndTime, lanes); finalizeRender(lanes, renderEndTime); } lanes = errorRetryLanes; @@ -1066,7 +1066,7 @@ export function performWorkOnRoot( if (exitStatus === RootFatalErrored) { if (enableProfilerTimer && enableComponentPerformanceTrack) { setCurrentTrackFromLanes(lanes); - logErroredRenderPhase(renderStartTime, renderEndTime); + logErroredRenderPhase(renderStartTime, renderEndTime, lanes); finalizeRender(lanes, renderEndTime); } prepareFreshStack(root, NoLanes); @@ -1207,7 +1207,7 @@ function finishConcurrentRender( // until we receive more data. if (enableProfilerTimer && enableComponentPerformanceTrack) { setCurrentTrackFromLanes(lanes); - logSuspendedRenderPhase(renderStartTime, renderEndTime); + logSuspendedRenderPhase(renderStartTime, renderEndTime, lanes); finalizeRender(lanes, renderEndTime); trackSuspendedTime(lanes, renderEndTime); } @@ -1757,9 +1757,17 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { // then this is considered a prewarm and not an interrupted render because // we couldn't have shown anything anyway so it's not a bad thing that we // got interrupted. - logSuspendedRenderPhase(previousRenderStartTime, renderStartTime); + logSuspendedRenderPhase( + previousRenderStartTime, + renderStartTime, + lanes, + ); } else { - logInterruptedRenderPhase(previousRenderStartTime, renderStartTime); + logInterruptedRenderPhase( + previousRenderStartTime, + renderStartTime, + lanes, + ); } finalizeRender(workInProgressRootRenderLanes, renderStartTime); } @@ -1783,6 +1791,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { : clampedUpdateTime >= 0 ? clampedUpdateTime : renderStartTime, + lanes, ); } logBlockingStart( @@ -1791,6 +1800,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { blockingEventType, blockingEventIsRepeat, renderStartTime, + lanes, ); clearBlockingTimers(); } @@ -1817,6 +1827,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { : clampedUpdateTime >= 0 ? clampedUpdateTime : renderStartTime, + lanes, ); } logTransitionStart( @@ -3202,9 +3213,13 @@ function commitRootImpl( // Log the previous render phase once we commit. I.e. we weren't interrupted. setCurrentTrackFromLanes(lanes); if (exitStatus === RootErrored) { - logErroredRenderPhase(completedRenderStartTime, completedRenderEndTime); + logErroredRenderPhase( + completedRenderStartTime, + completedRenderEndTime, + lanes, + ); } else { - logRenderPhase(completedRenderStartTime, completedRenderEndTime); + logRenderPhase(completedRenderStartTime, completedRenderEndTime, lanes); } } diff --git a/packages/scheduler/src/__tests__/Scheduler-test.js b/packages/scheduler/src/__tests__/Scheduler-test.js index 5af59a24a24f5..9e0813e461e61 100644 --- a/packages/scheduler/src/__tests__/Scheduler-test.js +++ b/packages/scheduler/src/__tests__/Scheduler-test.js @@ -183,7 +183,7 @@ describe('SchedulerBrowser', () => { it('task with continuation', () => { scheduleCallback(NormalPriority, () => { runtime.log('Task'); - // Request paint so that we yield at the end of the frame interval + // Request paint so that we yield immediately requestPaint(); while (!Scheduler.unstable_shouldYield()) { runtime.advanceTime(1); @@ -199,7 +199,7 @@ describe('SchedulerBrowser', () => { runtime.assertLog([ 'Message Event', 'Task', - gate(flags => (flags.www ? 'Yield at 10ms' : 'Yield at 5ms')), + 'Yield at 0ms', 'Post Message', ]); diff --git a/packages/scheduler/src/forks/Scheduler.js b/packages/scheduler/src/forks/Scheduler.js index a785bec95320c..0e85e0a99b7b0 100644 --- a/packages/scheduler/src/forks/Scheduler.js +++ b/packages/scheduler/src/forks/Scheduler.js @@ -93,6 +93,8 @@ var isPerformingWork = false; var isHostCallbackScheduled = false; var isHostTimeoutScheduled = false; +var needsPaint = false; + // Capture local references to native APIs, in case a polyfill overrides them. const localSetTimeout = typeof setTimeout === 'function' ? setTimeout : null; const localClearTimeout = @@ -456,6 +458,10 @@ let frameInterval = frameYieldMs; let startTime = -1; function shouldYieldToHost(): boolean { + if (needsPaint) { + // Yield now. + return true; + } const timeElapsed = getCurrentTime() - startTime; if (timeElapsed < frameInterval) { // The main thread has only been blocked for a really short amount of time; @@ -466,7 +472,9 @@ function shouldYieldToHost(): boolean { return true; } -function requestPaint() {} +function requestPaint() { + needsPaint = true; +} function forceFrameRate(fps: number) { if (fps < 0 || fps > 125) { @@ -486,6 +494,7 @@ function forceFrameRate(fps: number) { } const performWorkUntilDeadline = () => { + needsPaint = false; if (isMessageLoopRunning) { const currentTime = getCurrentTime(); // Keep track of the start time so we can measure how long the main thread diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index f28d123e61e5b..afac564cad28a 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -22,6 +22,8 @@ // when it rolls out to prod. We should remove these as soon as possible. // ----------------------------------------------------------------------------- +export const enableHydrationLaneScheduling = true; + // ----------------------------------------------------------------------------- // Land or remove (moderate effort) // @@ -111,8 +113,6 @@ export const enableSuspenseAvoidThisFallback = false; export const enableCPUSuspense = __EXPERIMENTAL__; -export const enableHydrationLaneScheduling = true; - // Test this at Meta before enabling. export const enableNoCloningMemoCache = false;