Use the JSX of the ViewTransition as the Stack Trace of "Animating" Traces (#34539)
Stacked on #34538. Track the Task of the first ViewTransition that we detected as animating. Use this as the Task as "Starting Animation", "Animating" etc. That way you can see which ViewTransition spawned the Animation. Although it's likely to be multiple. <img width="757" height="393" alt="Screenshot 2025-09-19 at 10 19 18 PM" src="https://github.com/user-attachments/assets/a6cdcb89-bd02-40ec-b3c3-11121c29e892" />
Sebastian Markbåge committed
Sep 20, 2025 at 11:11 UTC
d91d28c8ba6fe7c96e651f82fc47c9d5481bf5f9
3 files changed
+41
-31
packages/react-reconciler/src/ReactFiberCommitViewTransitions.js
+21
-10
@@ -39,6 +39,11 @@ import {
39
getViewTransitionName,
40
getViewTransitionClassName,
41
} from './ReactFiberViewTransitionComponent';
42
+import {trackAnimatingTask} from './ReactProfilerTimer';
43
+import {
44
+ enableComponentPerformanceTrack,
45
+ enableProfilerTimer,
46
+} from 'shared/ReactFeatureFlags';
47
48
export let shouldStartViewTransition: boolean = false;
49
@@ -101,21 +106,27 @@ export function popViewTransitionCancelableScope(
106
107
let viewTransitionHostInstanceIdx = 0;
108
104
-export function applyViewTransitionToHostInstances(
105
- child: null | Fiber,
109
+function applyViewTransitionToHostInstances(
110
+ fiber: Fiber,
111
name: string,
112
className: ?string,
113
collectMeasurements: null | Array<InstanceMeasurement>,
114
stopAtNestedViewTransitions: boolean,
115
): boolean {
116
viewTransitionHostInstanceIdx = 0;
112
- return applyViewTransitionToHostInstancesRecursive(
113
- child,
117
+ const inViewport = applyViewTransitionToHostInstancesRecursive(
118
+ fiber.child,
119
name,
120
className,
121
collectMeasurements,
122
stopAtNestedViewTransitions,
123
);
124
+ if (enableProfilerTimer && enableComponentPerformanceTrack && inViewport) {
125
+ if (fiber._debugTask != null) {
126
+ trackAnimatingTask(fiber._debugTask);
127
+ }
128
+ }
129
+ return inViewport;
130
}
131
132
function applyViewTransitionToHostInstancesRecursive(
@@ -247,7 +258,7 @@ function commitAppearingPairViewTransitions(placement: Fiber): void {
258
// We found a new appearing view transition with the same name as this deletion.
259
// We'll transition between them.
260
const inViewport = applyViewTransitionToHostInstances(
250
- child.child,
261
+ child,
262
name,
263
className,
264
null,
@@ -284,7 +295,7 @@ export function commitEnterViewTransitions(
295
);
296
if (className !== 'none') {
297
const inViewport = applyViewTransitionToHostInstances(
287
- placement.child,
298
+ placement,
299
name,
300
className,
301
null,
@@ -355,7 +366,7 @@ function commitDeletedPairViewTransitions(deletion: Fiber): void {
366
if (className !== 'none') {
367
// We found a new appearing view transition with the same name as this deletion.
368
const inViewport = applyViewTransitionToHostInstances(
358
- child.child,
369
+ child,
370
name,
371
className,
372
null,
@@ -406,7 +417,7 @@ export function commitExitViewTransitions(deletion: Fiber): void {
417
);
418
if (className !== 'none') {
419
const inViewport = applyViewTransitionToHostInstances(
409
- deletion.child,
420
+ deletion,
421
name,
422
className,
423
null,
@@ -490,7 +501,7 @@ export function commitBeforeUpdateViewTransition(
501
return;
502
}
503
applyViewTransitionToHostInstances(
493
- current.child,
504
+ current,
505
oldName,
506
className,
507
(current.memoizedState = []),
@@ -518,7 +529,7 @@ export function commitNestedViewTransitions(changedParent: Fiber): void {
529
child.flags &= ~Update;
530
if (className !== 'none') {
531
applyViewTransitionToHostInstances(
521
- child.child,
532
+ child,
533
name,
534
className,
535
(child.memoizedState = []),
packages/react-reconciler/src/ReactFiberWorkLoop.js
+11
-21
@@ -324,6 +324,7 @@ import {
324
animatingLanes,
325
retryClampTime,
326
idleClampTime,
327
+ animatingTask,
328
} from './ReactProfilerTimer';
329
330
// DEV stuff
@@ -1995,7 +1996,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1996
logAnimatingPhase(
1997
blockingClampTime,
1998
clampedRenderStartTime,
1998
- previousUpdateTask,
1999
+ animatingTask,
2000
);
2001
}
2002
logBlockingStart(
@@ -2048,7 +2049,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
2049
logAnimatingPhase(
2050
transitionClampTime,
2051
clampedRenderStartTime,
2051
- previousUpdateTask,
2052
+ animatingTask,
2053
);
2054
}
2055
logTransitionStart(
@@ -2069,14 +2070,14 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
2070
if (includesRetryLane(animatingLanes)) {
2071
// If this lane is still animating, log the time from previous render finishing to now as animating.
2072
setCurrentTrackFromLanes(SomeRetryLane);
2072
- logAnimatingPhase(retryClampTime, renderStartTime, previousUpdateTask);
2073
+ logAnimatingPhase(retryClampTime, renderStartTime, animatingTask);
2074
}
2075
}
2076
if (includesIdleGroupLanes(lanes)) {
2077
if (includesIdleGroupLanes(animatingLanes)) {
2078
// If this lane is still animating, log the time from previous render finishing to now as animating.
2079
setCurrentTrackFromLanes(IdleLane);
2079
- logAnimatingPhase(idleClampTime, renderStartTime, previousUpdateTask);
2080
+ logAnimatingPhase(idleClampTime, renderStartTime, animatingTask);
2081
}
2082
}
2083
}
@@ -3667,12 +3668,7 @@ function commitRoot(
3668
enableProfilerTimer ? suspendedViewTransition : (null: any),
3669
enableProfilerTimer
3670
? // This callback fires after "pendingEffects" so we need to snapshot the arguments.
3670
- finishedViewTransition.bind(
3671
- null,
3672
- lanes,
3673
- // TODO: Use a ViewTransition Task
3674
- __DEV__ ? workInProgressUpdateTask : null,
3675
- )
3671
+ finishedViewTransition.bind(null, lanes)
3672
: (null: any),
3673
);
3674
} else {
@@ -3712,15 +3708,13 @@ function suspendedViewTransition(reason: string): void {
3708
}
3709
}
3710
3715
-function finishedViewTransition(
3716
- lanes: Lanes,
3717
- task: null | ConsoleTask, // DEV-only
3718
-): void {
3711
+function finishedViewTransition(lanes: Lanes): void {
3712
if (enableProfilerTimer && enableComponentPerformanceTrack) {
3713
if ((animatingLanes & lanes) === NoLanes) {
3714
// Was already stopped by some other action or maybe other root.
3715
return;
3716
}
3717
+ const task = animatingTask;
3718
stopAnimating(lanes);
3719
// If an affected track isn't in the middle of rendering or committing, log from the previous
3720
// finished render until the end of the animation.
@@ -3835,7 +3829,7 @@ function flushLayoutEffects(): void {
3829
commitEndTime, // The start is the end of the first commit part.
3830
commitStartTime, // The end is the start of the second commit part.
3831
suspendedViewTransitionReason,
3838
- workInProgressUpdateTask, // TODO: Use a ViewTransition Task and this is not safe to read in this phase.
3832
+ animatingTask,
3833
);
3834
}
3835
}
@@ -3938,7 +3932,7 @@ function flushSpawnedWork(): void {
3932
startViewTransitionStartTime,
3933
commitEndTime,
3934
pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT,
3941
- workInProgressUpdateTask, // TODO: Use a ViewTransition Task.
3935
+ animatingTask,
3936
);
3937
if (pendingDelayedCommitReason !== ABORTED_VIEW_TRANSITION_COMMIT) {
3938
pendingDelayedCommitReason = ANIMATION_STARTED_COMMIT;
@@ -4440,11 +4434,7 @@ function flushPassiveEffectsImpl() {
4434
passiveEffectStartTime = now();
4435
if (pendingDelayedCommitReason === ANIMATION_STARTED_COMMIT) {
4436
// The animation was started, so we've been animating since that happened.
4443
- logAnimatingPhase(
4444
- commitEndTime,
4445
- passiveEffectStartTime,
4446
- workInProgressUpdateTask, // TODO: Use a ViewTransition Task
4447
- );
4437
+ logAnimatingPhase(commitEndTime, passiveEffectStartTime, animatingTask);
4438
} else {
4439
logPaintYieldPhase(
4440
commitEndTime,
packages/react-reconciler/src/ReactProfilerTimer.js
+9
@@ -93,6 +93,7 @@ export let retryClampTime: number = -0;
93
export let idleClampTime: number = -0;
94
95
export let animatingLanes: Lanes = NoLanes;
96
+export let animatingTask: null | ConsoleTask = null; // First ViewTransition applying an Animation.
97
98
export let yieldReason: SuspendedReason = (0: any);
99
export let yieldStartTime: number = -1.1; // The time when we yielded to the event loop
@@ -601,8 +602,16 @@ export function transferActualDuration(fiber: Fiber): void {
602
603
export function startAnimating(lanes: Lanes): void {
604
animatingLanes |= lanes;
605
+ animatingTask = null;
606
}
607
608
export function stopAnimating(lanes: Lanes): void {
609
animatingLanes &= ~lanes;
610
+ animatingTask = null;
611
+}
612
+
613
+export function trackAnimatingTask(task: ConsoleTask): void {
614
+ if (animatingTask === null) {
615
+ animatingTask = task;
616
+ }
617
}