Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fiber] Mark hydrated components in tertiary color (green) #31829

Merged
merged 3 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -1937,6 +1937,7 @@ const SUSPENDED_MARKER: SuspenseState = {
dehydrated: null,
treeContext: null,
retryLane: NoLane,
hydrationErrors: null,
};

function mountSuspenseOffscreenState(renderLanes: Lanes): OffscreenState {
Expand Down
81 changes: 81 additions & 0 deletions packages/react-reconciler/src/ReactFiberCommitWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ import {
FormReset,
Cloned,
PerformedWork,
ForceClientRender,
} from './ReactFiberFlags';
import {
commitStartTime,
Expand All @@ -113,6 +114,7 @@ import {
import {
logComponentRender,
logComponentEffect,
logSuspenseBoundaryClientRendered,
} from './ReactFiberPerformanceTrack';
import {ConcurrentMode, NoMode, ProfileMode} from './ReactTypeOfMode';
import {deferHiddenCallbacks} from './ReactFiberClassUpdateQueue';
Expand Down Expand Up @@ -2689,6 +2691,8 @@ function recursivelyTraversePassiveMountEffects(
}
}

let inHydratedSubtree = false;

function commitPassiveMountOnFiber(
finishedRoot: FiberRoot,
finishedWork: Fiber,
Expand All @@ -2713,6 +2717,7 @@ function commitPassiveMountOnFiber(
finishedWork,
((finishedWork.actualStartTime: any): number),
endTime,
inHydratedSubtree,
);
}

Expand Down Expand Up @@ -2741,13 +2746,29 @@ function commitPassiveMountOnFiber(
}
case HostRoot: {
const prevEffectDuration = pushNestedEffectDurations();

const wasInHydratedSubtree = inHydratedSubtree;
if (enableProfilerTimer && enableComponentPerformanceTrack) {
// Detect if this was a hydration commit by look at if the previous state was
// dehydrated and this wasn't a forced client render.
inHydratedSubtree =
finishedWork.alternate !== null &&
(finishedWork.alternate.memoizedState: RootState).isDehydrated &&
(finishedWork.flags & ForceClientRender) === NoFlags;
}

recursivelyTraversePassiveMountEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
endTime,
);

if (enableProfilerTimer && enableComponentPerformanceTrack) {
inHydratedSubtree = wasInHydratedSubtree;
}

if (flags & Passive) {
let previousCache: Cache | null = null;
if (finishedWork.alternate !== null) {
Expand Down Expand Up @@ -2841,6 +2862,64 @@ function commitPassiveMountOnFiber(
}
break;
}
case SuspenseComponent: {
const wasInHydratedSubtree = inHydratedSubtree;
if (enableProfilerTimer && enableComponentPerformanceTrack) {
const prevState: SuspenseState | null =
finishedWork.alternate !== null
? finishedWork.alternate.memoizedState
: null;
const nextState: SuspenseState | null = finishedWork.memoizedState;
if (
prevState !== null &&
prevState.dehydrated !== null &&
(nextState === null || nextState.dehydrated === null)
) {
// This was dehydrated but is no longer dehydrated. We may have now either hydrated it
// or client rendered it.
const deletions = finishedWork.deletions;
if (
deletions !== null &&
deletions.length > 0 &&
deletions[0].tag === DehydratedFragment
) {
// This was an abandoned hydration that deleted the dehydrated fragment. That means we
// are not hydrating this Suspense boundary.
inHydratedSubtree = false;
const hydrationErrors = prevState.hydrationErrors;
// If there were no hydration errors, that suggests that this was an intentional client
// rendered boundary. Such as postpone.
if (hydrationErrors !== null) {
const startTime: number = (finishedWork.actualStartTime: any);
logSuspenseBoundaryClientRendered(
finishedWork,
startTime,
endTime,
hydrationErrors,
);
}
} else {
// If any children committed they were hydrated.
inHydratedSubtree = true;
}
} else {
inHydratedSubtree = false;
}
}

recursivelyTraversePassiveMountEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
endTime,
);

if (enableProfilerTimer && enableComponentPerformanceTrack) {
inHydratedSubtree = wasInHydratedSubtree;
}
break;
}
case LegacyHiddenComponent: {
if (enableLegacyHidden) {
recursivelyTraversePassiveMountEffects(
Expand Down Expand Up @@ -3074,6 +3153,7 @@ export function reconnectPassiveEffects(
finishedWork,
((finishedWork.actualStartTime: any): number),
endTime,
inHydratedSubtree,
);
}

Expand Down Expand Up @@ -3317,6 +3397,7 @@ function commitAtomicPassiveEffects(
finishedWork,
((finishedWork.actualStartTime: any): number),
endTime,
inHydratedSubtree,
);
}

Expand Down
10 changes: 7 additions & 3 deletions packages/react-reconciler/src/ReactFiberCompleteWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -926,9 +926,13 @@ function completeDehydratedSuspenseBoundary(
// Successfully completed this tree. If this was a forced client render,
// there may have been recoverable errors during first hydration
// attempt. If so, add them to a queue so we can log them in the
// commit phase.
upgradeHydrationErrorsToRecoverable();

// commit phase. We also add them to prev state so we can get to them
// from the Suspense Boundary.
const hydrationErrors = upgradeHydrationErrorsToRecoverable();
if (current !== null && current.memoizedState !== null) {
const prevState: SuspenseState = current.memoizedState;
prevState.hydrationErrors = hydrationErrors;
}
// Fall through to normal Suspense path
return true;
}
Expand Down
11 changes: 8 additions & 3 deletions packages/react-reconciler/src/ReactFiberHydrationContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ function tryHydrateSuspense(fiber: Fiber, nextInstance: any) {
dehydrated: suspenseInstance,
treeContext: getSuspendedTreeContext(),
retryLane: OffscreenLane,
hydrationErrors: null,
};
fiber.memoizedState = suspenseState;
// Store the dehydrated fragment as a child fiber.
Expand Down Expand Up @@ -701,14 +702,18 @@ function resetHydrationState(): void {
didSuspendOrErrorDEV = false;
}

export function upgradeHydrationErrorsToRecoverable(): void {
if (hydrationErrors !== null) {
export function upgradeHydrationErrorsToRecoverable(): Array<
CapturedValue<mixed>,
> | null {
const queuedErrors = hydrationErrors;
if (queuedErrors !== null) {
// Successfully completed a forced client render. The errors that occurred
// during the hydration attempt are now recovered. We will log them in
// commit phase, once the entire tree has finished.
queueRecoverableErrors(hydrationErrors);
queueRecoverableErrors(queuedErrors);
hydrationErrors = null;
}
return queuedErrors;
}

function getIsHydrating(): boolean {
Expand Down
97 changes: 93 additions & 4 deletions packages/react-reconciler/src/ReactFiberPerformanceTrack.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import type {Fiber} from './ReactInternalTypes';

import type {Lanes} from './ReactFiberLane';

import type {CapturedValue} from './ReactCapturedValue';

import getComponentNameFromFiber from './getComponentNameFromFiber';

import {
Expand Down Expand Up @@ -123,6 +125,7 @@ export function logComponentRender(
fiber: Fiber,
startTime: number,
endTime: number,
wasHydrated: boolean,
): void {
const name = getComponentNameFromFiber(fiber);
if (name === null) {
Expand All @@ -138,18 +141,62 @@ export function logComponentRender(
}
reusableComponentDevToolDetails.color =
selfTime < 0.5
? 'primary-light'
? wasHydrated
? 'tertiary-light'
: 'primary-light'
: selfTime < 10
? 'primary'
? wasHydrated
? 'tertiary'
: 'primary'
: selfTime < 100
? 'primary-dark'
? wasHydrated
? 'tertiary-dark'
: 'primary-dark'
: 'error';
reusableComponentOptions.start = startTime;
reusableComponentOptions.end = endTime;
performance.measure(name, reusableComponentOptions);
}
}

export function logSuspenseBoundaryClientRendered(
fiber: Fiber,
startTime: number,
endTime: number,
errors: Array<CapturedValue<mixed>>,
): void {
if (supportsUserTiming) {
const properties = [];
if (__DEV__) {
for (let i = 0; i < errors.length; i++) {
const capturedValue = errors[i];
const error = capturedValue.value;
const message =
typeof error === 'object' &&
error !== null &&
typeof error.message === 'string'
? // eslint-disable-next-line react-internal/safe-string-coercion
String(error.message)
: // eslint-disable-next-line react-internal/safe-string-coercion
String(error);
properties.push(['Error', message]);
}
}
performance.measure('Suspense', {
start: startTime,
end: endTime,
detail: {
devtools: {
color: 'error',
track: COMPONENTS_TRACK,
tooltipText: 'Hydration failed',
properties,
},
},
});
}
}

export function logComponentEffect(
fiber: Fiber,
startTime: number,
Expand Down Expand Up @@ -387,6 +434,48 @@ export function logSuspendedWithDelayPhase(
}
}

export function logRecoveredRenderPhase(
startTime: number,
endTime: number,
lanes: Lanes,
recoverableErrors: Array<CapturedValue<mixed>>,
hydrationFailed: boolean,
): void {
if (supportsUserTiming) {
const properties = [];
if (__DEV__) {
for (let i = 0; i < recoverableErrors.length; i++) {
const capturedValue = recoverableErrors[i];
const error = capturedValue.value;
const message =
typeof error === 'object' &&
error !== null &&
typeof error.message === 'string'
? // eslint-disable-next-line react-internal/safe-string-coercion
String(error.message)
: // eslint-disable-next-line react-internal/safe-string-coercion
String(error);
properties.push(['Recoverable Error', message]);
}
}
performance.measure('Recovered', {
start: startTime,
end: endTime,
detail: {
devtools: {
color: 'primary-dark',
track: reusableLaneDevToolDetails.track,
trackGroup: LANES_TRACK_GROUP,
tooltipText: hydrationFailed
? 'Hydration Failed'
: 'Recovered after Error',
properties,
},
},
});
}
}

export function logErroredRenderPhase(
startTime: number,
endTime: number,
Expand All @@ -396,7 +485,7 @@ export function logErroredRenderPhase(
reusableLaneDevToolDetails.color = 'error';
reusableLaneOptions.start = startTime;
reusableLaneOptions.end = endTime;
performance.measure('Errored Render', reusableLaneOptions);
performance.measure('Errored', reusableLaneOptions);
}
}

Expand Down
3 changes: 3 additions & 0 deletions packages/react-reconciler/src/ReactFiberSuspenseComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {Fiber} from './ReactInternalTypes';
import type {SuspenseInstance} from './ReactFiberConfig';
import type {Lane} from './ReactFiberLane';
import type {TreeContext} from './ReactFiberTreeContext';
import type {CapturedValue} from './ReactCapturedValue';

import {SuspenseComponent, SuspenseListComponent} from './ReactWorkTags';
import {NoFlags, DidCapture} from './ReactFiberFlags';
Expand Down Expand Up @@ -49,6 +50,8 @@ export type SuspenseState = {
// OffscreenLane is the default for dehydrated boundaries.
// NoLane is the default for normal boundaries, which turns into "normal" pri.
retryLane: Lane,
// Stashed Errors that happened while attempting to hydrate this boundary.
hydrationErrors: Array<CapturedValue<mixed>> | null,
};

export type SuspenseListTailMode = 'collapsed' | 'hidden' | void;
Expand Down
15 changes: 15 additions & 0 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
} from './ReactFiberTracingMarkerComponent';
import type {OffscreenInstance} from './ReactFiberActivityComponent';
import type {Resource} from './ReactFiberConfig';
import type {RootState} from './ReactFiberRoot';

import {
enableCreateEventHandleAPI,
Expand Down Expand Up @@ -60,6 +61,7 @@ import {
logRenderPhase,
logInterruptedRenderPhase,
logSuspendedRenderPhase,
logRecoveredRenderPhase,
logErroredRenderPhase,
logInconsistentRender,
logSuspendedWithDelayPhase,
Expand Down Expand Up @@ -3184,6 +3186,19 @@ function commitRootImpl(
completedRenderEndTime,
lanes,
);
} else if (recoverableErrors !== null) {
const hydrationFailed =
finishedWork !== null &&
finishedWork.alternate !== null &&
(finishedWork.alternate.memoizedState: RootState).isDehydrated &&
(finishedWork.flags & ForceClientRender) !== NoFlags;
logRecoveredRenderPhase(
completedRenderStartTime,
completedRenderEndTime,
lanes,
recoverableErrors,
hydrationFailed,
);
} else {
logRenderPhase(completedRenderStartTime, completedRenderEndTime, lanes);
}
Expand Down
Loading