Skip to content

Commit ad578aa

Browse files
authored
Log Suspended startViewTransition Phase (#34511)
Stacked on #34510. The "Commit" phase for a View Transition starts before the snapshot phase (before mutation) and then stretches into the async gap of `startViewTransition`, encompasses the mutation phase inside of its update callback and finally the layout phase. However, between the mutation phase and the layout phase we may suspend the start of the view transition on fonts and/or images. In that case we now split the Commit phase into first one before we suspend and then we log "Waiting for Images and/or Fonts" and then another Commit phase around the layout effects. <img width="897" height="119" alt="Screenshot 2025-09-16 at 11 37 26 PM" src="https://github.com/user-attachments/assets/0fe21388-bb48-4456-a594-62227d12d9b7" />
1 parent 03a96c7 commit ad578aa

File tree

5 files changed

+90
-1
lines changed

5 files changed

+90
-1
lines changed

packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ import {
125125
enableViewTransition,
126126
enableHydrationChangeEvent,
127127
enableFragmentRefsScrollIntoView,
128+
enableProfilerTimer,
128129
} from 'shared/ReactFeatureFlags';
129130
import {
130131
HostComponent,
@@ -2098,6 +2099,7 @@ export function startViewTransition(
20982099
spawnedWorkCallback: () => void,
20992100
passiveCallback: () => mixed,
21002101
errorCallback: mixed => void,
2102+
blockedCallback: string => void, // Profiling-only
21012103
): null | RunningViewTransition {
21022104
const ownerDocument: Document =
21032105
rootContainer.nodeType === DOCUMENT_NODE
@@ -2131,10 +2133,10 @@ export function startViewTransition(
21312133
blockingPromises.push(ownerDocument.fonts.ready);
21322134
}
21332135
}
2136+
const blockingIndexSnapshot = blockingPromises.length;
21342137
if (suspendedState !== null) {
21352138
// Suspend on any images that still haven't loaded and are in the viewport.
21362139
const suspenseyImages = suspendedState.suspenseyImages;
2137-
const blockingIndexSnapshot = blockingPromises.length;
21382140
let imgBytes = 0;
21392141
for (let i = 0; i < suspenseyImages.length; i++) {
21402142
const suspenseyImage = suspenseyImages[i];
@@ -2162,6 +2164,15 @@ export function startViewTransition(
21622164
}
21632165
}
21642166
if (blockingPromises.length > 0) {
2167+
if (enableProfilerTimer) {
2168+
const blockedReason =
2169+
blockingIndexSnapshot > 0
2170+
? blockingPromises.length > blockingIndexSnapshot
2171+
? 'Waiting on Fonts and Images'
2172+
: 'Waiting on Fonts'
2173+
: 'Waiting on Images';
2174+
blockedCallback(blockedReason);
2175+
}
21652176
const blockingReady = Promise.race([
21662177
Promise.all(blockingPromises),
21672178
new Promise(resolve =>

packages/react-native-renderer/src/ReactFiberConfigNative.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,7 @@ export function startViewTransition(
673673
spawnedWorkCallback: () => void,
674674
passiveCallback: () => mixed,
675675
errorCallback: mixed => void,
676+
blockedCallback: string => void, // Profiling-only
676677
): null | RunningViewTransition {
677678
mutationCallback();
678679
layoutCallback();

packages/react-reconciler/src/ReactFiberPerformanceTrack.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1254,6 +1254,45 @@ export function logSuspendedCommitPhase(
12541254
}
12551255
}
12561256

1257+
export function logSuspendedViewTransitionPhase(
1258+
startTime: number,
1259+
endTime: number,
1260+
reason: string,
1261+
debugTask: null | ConsoleTask,
1262+
): void {
1263+
// This means the commit was suspended on CSS or images.
1264+
if (supportsUserTiming) {
1265+
if (endTime <= startTime) {
1266+
return;
1267+
}
1268+
// TODO: Include the exact reason and URLs of what resources suspended.
1269+
// TODO: This might also be Suspended while waiting on a View Transition.
1270+
if (__DEV__ && debugTask) {
1271+
debugTask.run(
1272+
// $FlowFixMe[method-unbinding]
1273+
console.timeStamp.bind(
1274+
console,
1275+
reason,
1276+
startTime,
1277+
endTime,
1278+
currentTrack,
1279+
LANES_TRACK_GROUP,
1280+
'secondary-light',
1281+
),
1282+
);
1283+
} else {
1284+
console.timeStamp(
1285+
reason,
1286+
startTime,
1287+
endTime,
1288+
currentTrack,
1289+
LANES_TRACK_GROUP,
1290+
'secondary-light',
1291+
);
1292+
}
1293+
}
1294+
}
1295+
12571296
export function logCommitErrored(
12581297
startTime: number,
12591298
endTime: number,

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import {
8181
logSuspendedWithDelayPhase,
8282
logSuspenseThrottlePhase,
8383
logSuspendedCommitPhase,
84+
logSuspendedViewTransitionPhase,
8485
logCommitPhase,
8586
logPaintYieldPhase,
8687
logStartViewTransitionYieldPhase,
@@ -704,6 +705,7 @@ let pendingTransitionTypes: null | TransitionTypes = null;
704705
let pendingDidIncludeRenderPhaseUpdate: boolean = false;
705706
let pendingSuspendedCommitReason: SuspendedCommitReason = IMMEDIATE_COMMIT; // Profiling-only
706707
let pendingDelayedCommitReason: DelayedCommitReason = IMMEDIATE_COMMIT; // Profiling-only
708+
let pendingSuspendedViewTransitionReason: null | string = null; // Profiling-only
707709

708710
// Use these to prevent an infinite loop of nested updates
709711
const NESTED_UPDATE_LIMIT = 50;
@@ -3445,6 +3447,7 @@ function commitRoot(
34453447
pendingEffectsRenderEndTime = completedRenderEndTime;
34463448
pendingSuspendedCommitReason = suspendedCommitReason;
34473449
pendingDelayedCommitReason = IMMEDIATE_COMMIT;
3450+
pendingSuspendedViewTransitionReason = null;
34483451
}
34493452

34503453
if (enableGestureTransition && isGestureRender(lanes)) {
@@ -3604,6 +3607,7 @@ function commitRoot(
36043607
flushSpawnedWork,
36053608
flushPassiveEffects,
36063609
reportViewTransitionError,
3610+
enableProfilerTimer ? suspendedViewTransition : (null: any),
36073611
);
36083612
} else {
36093613
// Flush synchronously.
@@ -3624,6 +3628,24 @@ function reportViewTransitionError(error: mixed) {
36243628
onRecoverableError(error, makeErrorInfo(null));
36253629
}
36263630

3631+
function suspendedViewTransition(reason: string): void {
3632+
if (enableProfilerTimer && enableComponentPerformanceTrack) {
3633+
// We'll split the commit into two phases, because we're suspended in the middle.
3634+
recordCommitEndTime();
3635+
logCommitPhase(
3636+
pendingSuspendedCommitReason === IMMEDIATE_COMMIT
3637+
? pendingEffectsRenderEndTime
3638+
: commitStartTime,
3639+
commitEndTime,
3640+
commitErrors,
3641+
pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT,
3642+
workInProgressUpdateTask,
3643+
);
3644+
pendingSuspendedViewTransitionReason = reason;
3645+
pendingSuspendedCommitReason = SUSPENDED_COMMIT;
3646+
}
3647+
}
3648+
36273649
function flushAfterMutationEffects(): void {
36283650
if (pendingEffectsStatus !== PENDING_AFTER_MUTATION_PHASE) {
36293651
return;
@@ -3688,6 +3710,21 @@ function flushLayoutEffects(): void {
36883710
}
36893711
pendingEffectsStatus = NO_PENDING_EFFECTS;
36903712

3713+
if (enableProfilerTimer && enableComponentPerformanceTrack) {
3714+
const suspendedViewTransitionReason = pendingSuspendedViewTransitionReason;
3715+
if (suspendedViewTransitionReason !== null) {
3716+
// We suspended in the middle of the commit for the view transition.
3717+
// We'll start a new commit track now.
3718+
recordCommitTime();
3719+
logSuspendedViewTransitionPhase(
3720+
commitEndTime, // The start is the end of the first commit part.
3721+
commitStartTime, // The end is the start of the second commit part.
3722+
suspendedViewTransitionReason,
3723+
workInProgressUpdateTask,
3724+
);
3725+
}
3726+
}
3727+
36913728
const root = pendingEffectsRoot;
36923729
const finishedWork = pendingFinishedWork;
36933730
const lanes = pendingEffectsLanes;

packages/react-test-renderer/src/ReactFiberConfigTestHost.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ export function startViewTransition(
423423
spawnedWorkCallback: () => void,
424424
passiveCallback: () => mixed,
425425
errorCallback: mixed => void,
426+
blockedCallback: string => void, // Profiling-only
426427
): null | RunningViewTransition {
427428
mutationCallback();
428429
layoutCallback();

0 commit comments

Comments
 (0)