@samitouri / QOS-React / commits / d91d28c8ba

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 }