Log Custom Reason for the Suspended Commit Track (#34522)
Stacked on #34511. We currently log all Suspended Commit as "Suspended on Images or CSS" but it can really be other reasons too now. Like waiting on the previous View Transition. This allows the host config configure this reason. Now when one animation starts before another one finishes we log that as "Waiting for the previous Animation". <img width="592" height="257" alt="Screenshot 2025-09-17 at 11 53 45 PM" src="https://github.com/user-attachments/assets/817af8b5-37ae-46d8-bfd1-cd3fc637f3f3" />
Sebastian Markbåge committed
Sep 20, 2025 at 11:01 UTC
b204edda3aa397243faa267512811b778542f6a5
10 files changed
+80
-61
packages/react-art/src/ReactFiberConfigART.js
+4
@@ -621,6 +621,10 @@ export function waitForCommitToBeReady(timeoutOffset) {
621
return null;
622
}
623
624
+export function getSuspendedCommitReason(state, rootContainer) {
625
+ return null;
626
+}
627
+
628
export const NotPendingTransition = null;
629
export const HostTransitionContext: ReactContext<TransitionStatus> = {
630
$$typeof: REACT_CONTEXT_TYPE,
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+25
@@ -5965,6 +5965,7 @@ export opaque type SuspendedState = {
5965
imgBytes: number, // number of bytes we estimate needing to download
5966
suspenseyImages: Array<HTMLImageElement>, // instances of suspensey images (whether loaded or not)
5967
waitingForImages: boolean, // false when we're no longer blocking on images
5968
+ waitingForViewTransition: boolean,
5969
unsuspend: null | (() => void),
5970
};
5971
@@ -5976,6 +5977,7 @@ export function startSuspendingCommit(): SuspendedState {
5977
imgBytes: 0,
5978
suspenseyImages: [],
5979
waitingForImages: true,
5980
+ waitingForViewTransition: false,
5981
// We use a noop function when we begin suspending because if possible we want the
5982
// waitfor step to finish synchronously. If it doesn't we'll return a function to
5983
// provide the actual unsuspend function and that will get completed when the count
@@ -6123,6 +6125,7 @@ export function suspendOnActiveViewTransition(
6125
return;
6126
}
6127
state.count++;
6128
+ state.waitingForViewTransition = true;
6129
const ping = onUnsuspend.bind(state);
6130
activeViewTransition.finished.then(ping, ping);
6131
}
@@ -6206,6 +6209,28 @@ export function waitForCommitToBeReady(
6209
return null;
6210
}
6211
6212
+export function getSuspendedCommitReason(
6213
+ state: SuspendedState,
6214
+ rootContainer: Container,
6215
+): null | string {
6216
+ if (state.waitingForViewTransition) {
6217
+ return 'Waiting for the previous Animation';
6218
+ }
6219
+ if (state.count > 0) {
6220
+ if (state.imgCount > 0) {
6221
+ return 'Suspended on CSS and Images';
6222
+ }
6223
+ return 'Suspended on CSS';
6224
+ }
6225
+ if (state.imgCount === 1) {
6226
+ return 'Suspended on an Image';
6227
+ }
6228
+ if (state.imgCount > 0) {
6229
+ return 'Suspended on Images';
6230
+ }
6231
+ return null;
6232
+}
6233
+
6234
function checkIfFullyUnsuspended(state: SuspendedState) {
6235
if (state.count === 0 && (state.imgCount === 0 || !state.waitingForImages)) {
6236
if (state.stylesheets) {
packages/react-native-renderer/src/ReactFiberConfigFabric.js
+7
@@ -627,6 +627,13 @@ export function waitForCommitToBeReady(
627
return null;
628
}
629
630
+export function getSuspendedCommitReason(
631
+ state: SuspendedState,
632
+ rootContainer: Container,
633
+): null | string {
634
+ return null;
635
+}
636
+
637
export type FragmentInstanceType = {
638
_fragmentFiber: Fiber,
639
_observers: null | Set<IntersectionObserver>,
packages/react-native-renderer/src/ReactFiberConfigNative.js
+7
@@ -806,6 +806,13 @@ export function waitForCommitToBeReady(
806
return null;
807
}
808
809
+export function getSuspendedCommitReason(
810
+ state: SuspendedState,
811
+ rootContainer: Container,
812
+): null | string {
813
+ return null;
814
+}
815
+
816
export const NotPendingTransition: TransitionStatus = null;
817
export const HostTransitionContext: ReactContext<TransitionStatus> = {
818
$$typeof: REACT_CONTEXT_TYPE,
packages/react-noop-renderer/src/createReactNoop.js
+7
@@ -702,6 +702,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
702
703
waitForCommitToBeReady,
704
705
+ getSuspendedCommitReason(
706
+ state: SuspendedState,
707
+ rootContainer: Container,
708
+ ): null | string {
709
+ return null;
710
+ },
711
+
712
NotPendingTransition: (null: TransitionStatus),
713
714
resetFormInstance(form: Instance) {},
packages/react-reconciler/src/ReactFiberPerformanceTrack.js
+3
-38
@@ -1180,45 +1180,10 @@ export function logInconsistentRender(
1180
}
1181
}
1182
1183
-export function logSuspenseThrottlePhase(
1184
- startTime: number,
1185
- endTime: number,
1186
- debugTask: null | ConsoleTask,
1187
-): void {
1188
- // This was inside a throttled Suspense boundary commit.
1189
- if (supportsUserTiming) {
1190
- if (endTime <= startTime) {
1191
- return;
1192
- }
1193
- if (__DEV__ && debugTask) {
1194
- debugTask.run(
1195
- // $FlowFixMe[method-unbinding]
1196
- console.timeStamp.bind(
1197
- console,
1198
- 'Throttled',
1199
- startTime,
1200
- endTime,
1201
- currentTrack,
1202
- LANES_TRACK_GROUP,
1203
- 'secondary-light',
1204
- ),
1205
- );
1206
- } else {
1207
- console.timeStamp(
1208
- 'Throttled',
1209
- startTime,
1210
- endTime,
1211
- currentTrack,
1212
- LANES_TRACK_GROUP,
1213
- 'secondary-light',
1214
- );
1215
- }
1216
- }
1217
-}
1218
-
1183
export function logSuspendedCommitPhase(
1184
startTime: number,
1185
endTime: number,
1186
+ reason: string,
1187
debugTask: null | ConsoleTask,
1188
): void {
1189
// This means the commit was suspended on CSS or images.
@@ -1233,7 +1198,7 @@ export function logSuspendedCommitPhase(
1198
// $FlowFixMe[method-unbinding]
1199
console.timeStamp.bind(
1200
console,
1236
- 'Suspended on CSS or Images',
1201
+ reason,
1202
startTime,
1203
endTime,
1204
currentTrack,
@@ -1243,7 +1208,7 @@ export function logSuspendedCommitPhase(
1208
);
1209
} else {
1210
console.timeStamp(
1246
- 'Suspended on CSS or Images',
1211
+ reason,
1212
startTime,
1213
endTime,
1214
currentTrack,
packages/react-reconciler/src/ReactFiberWorkLoop.js
+16
-23
@@ -79,7 +79,6 @@ import {
79
logErroredRenderPhase,
80
logInconsistentRender,
81
logSuspendedWithDelayPhase,
82
- logSuspenseThrottlePhase,
82
logSuspendedCommitPhase,
83
logSuspendedViewTransitionPhase,
84
logCommitPhase,
@@ -103,6 +102,7 @@ import {
102
startSuspendingCommit,
103
suspendOnActiveViewTransition,
104
waitForCommitToBeReady,
105
+ getSuspendedCommitReason,
106
preloadInstance,
107
preloadResource,
108
supportsHydration,
@@ -672,12 +672,10 @@ export function getRenderTargetTime(): number {
672
673
let legacyErrorBoundariesThatAlreadyFailed: Set<mixed> | null = null;
674
675
-type SuspendedCommitReason = 0 | 1 | 2;
676
-const IMMEDIATE_COMMIT = 0;
677
-const SUSPENDED_COMMIT = 1;
678
-const THROTTLED_COMMIT = 2;
675
+type SuspendedCommitReason = null | string;
676
677
type DelayedCommitReason = 0 | 1 | 2 | 3;
678
+const IMMEDIATE_COMMIT = 0;
679
const ABORTED_VIEW_TRANSITION_COMMIT = 1;
680
const DELAYED_PASSIVE_COMMIT = 2;
681
const ANIMATION_STARTED_COMMIT = 3;
@@ -703,7 +701,7 @@ let pendingViewTransitionEvents: Array<(types: Array<string>) => void> | null =
701
null;
702
let pendingTransitionTypes: null | TransitionTypes = null;
703
let pendingDidIncludeRenderPhaseUpdate: boolean = false;
706
-let pendingSuspendedCommitReason: SuspendedCommitReason = IMMEDIATE_COMMIT; // Profiling-only
704
+let pendingSuspendedCommitReason: SuspendedCommitReason = null; // Profiling-only
705
let pendingDelayedCommitReason: DelayedCommitReason = IMMEDIATE_COMMIT; // Profiling-only
706
let pendingSuspendedViewTransitionReason: null | string = null; // Profiling-only
707
@@ -1391,7 +1389,7 @@ function finishConcurrentRender(
1389
workInProgressSuspendedRetryLanes,
1390
exitStatus,
1391
null,
1394
- IMMEDIATE_COMMIT,
1392
+ null,
1393
renderStartTime,
1394
renderEndTime,
1395
);
@@ -1442,7 +1440,7 @@ function finishConcurrentRender(
1440
workInProgressSuspendedRetryLanes,
1441
workInProgressRootDidSkipSuspendedSiblings,
1442
exitStatus,
1445
- THROTTLED_COMMIT,
1443
+ 'Throttled',
1444
renderStartTime,
1445
renderEndTime,
1446
),
@@ -1463,7 +1461,7 @@ function finishConcurrentRender(
1461
workInProgressSuspendedRetryLanes,
1462
workInProgressRootDidSkipSuspendedSiblings,
1463
exitStatus,
1466
- IMMEDIATE_COMMIT,
1464
+ null,
1465
renderStartTime,
1466
renderEndTime,
1467
);
@@ -1555,7 +1553,9 @@ function commitRootWhenReady(
1553
suspendedRetryLanes,
1554
exitStatus,
1555
suspendedState,
1558
- SUSPENDED_COMMIT,
1556
+ enableProfilerTimer
1557
+ ? getSuspendedCommitReason(suspendedState, root.containerInfo)
1558
+ : null,
1559
completedRenderStartTime,
1560
completedRenderEndTime,
1561
),
@@ -3458,7 +3458,7 @@ function commitRoot(
3458
recoverableErrors,
3459
suspendedState,
3460
enableProfilerTimer
3461
- ? suspendedCommitReason === IMMEDIATE_COMMIT
3461
+ ? suspendedCommitReason === null
3462
? completedRenderEndTime
3463
: commitStartTime
3464
: 0,
@@ -3530,16 +3530,11 @@ function commitRoot(
3530
resetCommitErrors();
3531
recordCommitTime();
3532
if (enableComponentPerformanceTrack) {
3533
- if (suspendedCommitReason === SUSPENDED_COMMIT) {
3533
+ if (suspendedCommitReason !== null) {
3534
logSuspendedCommitPhase(
3535
completedRenderEndTime,
3536
commitStartTime,
3537
- workInProgressUpdateTask,
3538
- );
3539
- } else if (suspendedCommitReason === THROTTLED_COMMIT) {
3540
- logSuspenseThrottlePhase(
3541
- completedRenderEndTime,
3542
- commitStartTime,
3537
+ suspendedCommitReason,
3538
workInProgressUpdateTask,
3539
);
3540
}
@@ -3633,7 +3628,7 @@ function suspendedViewTransition(reason: string): void {
3628
// We'll split the commit into two phases, because we're suspended in the middle.
3629
recordCommitEndTime();
3630
logCommitPhase(
3636
- pendingSuspendedCommitReason === IMMEDIATE_COMMIT
3631
+ pendingSuspendedCommitReason === null
3632
? pendingEffectsRenderEndTime
3633
: commitStartTime,
3634
commitEndTime,
@@ -3642,7 +3637,7 @@ function suspendedViewTransition(reason: string): void {
3637
workInProgressUpdateTask,
3638
);
3639
pendingSuspendedViewTransitionReason = reason;
3645
- pendingSuspendedCommitReason = SUSPENDED_COMMIT;
3640
+ pendingSuspendedCommitReason = reason;
3641
}
3642
}
3643
@@ -3792,9 +3787,7 @@ function flushLayoutEffects(): void {
3787
if (enableProfilerTimer && enableComponentPerformanceTrack) {
3788
recordCommitEndTime();
3789
logCommitPhase(
3795
- suspendedCommitReason === IMMEDIATE_COMMIT
3796
- ? completedRenderEndTime
3797
- : commitStartTime,
3790
+ suspendedCommitReason === null ? completedRenderEndTime : commitStartTime,
3791
commitEndTime,
3792
commitErrors,
3793
pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT,
packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
+3
@@ -114,6 +114,9 @@ describe('ReactFiberHostContext', () => {
114
waitForCommitToBeReady(state, timeoutOffset) {
115
return null;
116
},
117
+ getSuspendedCommitReason(state, rootContainer) {
118
+ return null;
119
+ },
120
supportsMutation: true,
121
});
122
packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
+1
@@ -99,6 +99,7 @@ export const suspendInstance = $$$config.suspendInstance;
99
export const suspendOnActiveViewTransition =
100
$$$config.suspendOnActiveViewTransition;
101
export const waitForCommitToBeReady = $$$config.waitForCommitToBeReady;
102
+export const getSuspendedCommitReason = $$$config.getSuspendedCommitReason;
103
export const NotPendingTransition = $$$config.NotPendingTransition;
104
export const HostTransitionContext = $$$config.HostTransitionContext;
105
export const resetFormInstance = $$$config.resetFormInstance;
packages/react-test-renderer/src/ReactFiberConfigTestHost.js
+7
@@ -589,6 +589,13 @@ export function waitForCommitToBeReady(
589
return null;
590
}
591
592
+export function getSuspendedCommitReason(
593
+ state: SuspendedState,
594
+ rootContainer: Container,
595
+): null | string {
596
+ return null;
597
+}
598
+
599
export const NotPendingTransition: TransitionStatus = null;
600
export const HostTransitionContext: ReactContext<TransitionStatus> = {
601
$$typeof: REACT_CONTEXT_TYPE,