Skip to content

Commit

Permalink
[Fiber] Mark hydrated components in tertiary color (green) (facebook#…
Browse files Browse the repository at this point in the history
…31829)

This is a follow up to facebook#31752.

This keeps track in the commit phase whether this subtree was hydrated.
If it was, then we mark those components in the Components track as
green. Just like the phase itself is marked as green.

If the boundary client rendered we instead mark it as "errored" and its
children given the plain primary render color (blue). I also collect the
hydration error for this case so we can include its message in the
details view. (Unfortunately this doesn't support newlines atm.)

Most of the time this happens in separate commits for each boundary but
it is possible to force a client render in the same pass as a hydration.
Such as if an update flows into a boundary that has been put into
fallback state after it was initially attempted.

<img width="1487" alt="Screenshot 2024-12-18 at 12 06 54 AM"
src="https://github.com/user-attachments/assets/74c57291-4d11-414c-9751-3dac3285a89a"
/>
  • Loading branch information
sebmarkbage authored Dec 19, 2024
1 parent 7de040c commit 17520b6
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 10 deletions.
1 change: 1 addition & 0 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -1933,6 +1933,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 @@ -59,6 +60,7 @@ import {
logRenderPhase,
logInterruptedRenderPhase,
logSuspendedRenderPhase,
logRecoveredRenderPhase,
logErroredRenderPhase,
logInconsistentRender,
logSuspendedWithDelayPhase,
Expand Down Expand Up @@ -3183,6 +3185,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

0 comments on commit 17520b6

Please sign in to comment.