Repository: facebook/react
Pull request: Use the JSX of the ViewTransition as the Stack Trace of "Animating" Traces

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" />


Changed files:
filename: packages/react-reconciler/src/ReactFiberCommitViewTransitions.js
patch: @@ -39,6 +39,11 @@ import {
   getViewTransitionName,
   getViewTransitionClassName,
 } from './ReactFiberViewTransitionComponent';
+import {trackAnimatingTask} from './ReactProfilerTimer';
+import {
+  enableComponentPerformanceTrack,
+  enableProfilerTimer,
+} from 'shared/ReactFeatureFlags';
 
 export let shouldStartViewTransition: boolean = false;
 
@@ -101,21 +106,27 @@ export function popViewTransitionCancelableScope(
 
 let viewTransitionHostInstanceIdx = 0;
 
-export function applyViewTransitionToHostInstances(
-  child: null | Fiber,
+function applyViewTransitionToHostInstances(
+  fiber: Fiber,
   name: string,
   className: ?string,
   collectMeasurements: null | Array<InstanceMeasurement>,
   stopAtNestedViewTransitions: boolean,
 ): boolean {
   viewTransitionHostInstanceIdx = 0;
-  return applyViewTransitionToHostInstancesRecursive(
-    child,
+  const inViewport = applyViewTransitionToHostInstancesRecursive(
+    fiber.child,
     name,
     className,
     collectMeasurements,
     stopAtNestedViewTransitions,
   );
+  if (enableProfilerTimer && enableComponentPerformanceTrack && inViewport) {
+    if (fiber._debugTask != null) {
+      trackAnimatingTask(fiber._debugTask);
+    }
+  }
+  return inViewport;
 }
 
 function applyViewTransitionToHostInstancesRecursive(
@@ -247,7 +258,7 @@ function commitAppearingPairViewTransitions(placement: Fiber): void {
             // We found a new appearing view transition with the same name as this deletion.
             // We'll transition between them.
             const inViewport = applyViewTransitionToHostInstances(
-              child.child,
+              child,
               name,
               className,
               null,
@@ -284,7 +295,7 @@ export function commitEnterViewTransitions(
     );
     if (className !== 'none') {
       const inViewport = applyViewTransitionToHostInstances(
-        placement.child,
+        placement,
         name,
         className,
         null,
@@ -355,7 +366,7 @@ function commitDeletedPairViewTransitions(deletion: Fiber): void {
             if (className !== 'none') {
               // We found a new appearing view transition with the same name as this deletion.
               const inViewport = applyViewTransitionToHostInstances(
-                child.child,
+                child,
                 name,
                 className,
                 null,
@@ -406,7 +417,7 @@ export function commitExitViewTransitions(deletion: Fiber): void {
     );
     if (className !== 'none') {
       const inViewport = applyViewTransitionToHostInstances(
-        deletion.child,
+        deletion,
         name,
         className,
         null,
@@ -490,7 +501,7 @@ export function commitBeforeUpdateViewTransition(
     return;
   }
   applyViewTransitionToHostInstances(
-    current.child,
+    current,
     oldName,
     className,
     (current.memoizedState = []),
@@ -518,7 +529,7 @@ export function commitNestedViewTransitions(changedParent: Fiber): void {
       child.flags &= ~Update;
       if (className !== 'none') {
         applyViewTransitionToHostInstances(
-          child.child,
+          child,
           name,
           className,
           (child.memoizedState = []),

filename: packages/react-reconciler/src/ReactFiberWorkLoop.js
patch: @@ -324,6 +324,7 @@ import {
   animatingLanes,
   retryClampTime,
   idleClampTime,
+  animatingTask,
 } from './ReactProfilerTimer';
 
 // DEV stuff
@@ -1995,7 +1996,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
         logAnimatingPhase(
           blockingClampTime,
           clampedRenderStartTime,
-          previousUpdateTask,
+          animatingTask,
         );
       }
       logBlockingStart(
@@ -2048,7 +2049,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
         logAnimatingPhase(
           transitionClampTime,
           clampedRenderStartTime,
-          previousUpdateTask,
+          animatingTask,
         );
       }
       logTransitionStart(
@@ -2069,14 +2070,14 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
       if (includesRetryLane(animatingLanes)) {
         // If this lane is still animating, log the time from previous render finishing to now as animating.
         setCurrentTrackFromLanes(SomeRetryLane);
-        logAnimatingPhase(retryClampTime, renderStartTime, previousUpdateTask);
+        logAnimatingPhase(retryClampTime, renderStartTime, animatingTask);
       }
     }
     if (includesIdleGroupLanes(lanes)) {
       if (includesIdleGroupLanes(animatingLanes)) {
         // If this lane is still animating, log the time from previous render finishing to now as animating.
         setCurrentTrackFromLanes(IdleLane);
-        logAnimatingPhase(idleClampTime, renderStartTime, previousUpdateTask);
+        logAnimatingPhase(idleClampTime, renderStartTime, animatingTask);
       }
     }
   }
@@ -3667,12 +3668,7 @@ function commitRoot(
       enableProfilerTimer ? suspendedViewTransition : (null: any),
       enableProfilerTimer
         ? // This callback fires after "pendingEffects" so we need to snapshot the arguments.
-          finishedViewTransition.bind(
-            null,
-            lanes,
-            // TODO: Use a ViewTransition Task
-            __DEV__ ? workInProgressUpdateTask : null,
-          )
+          finishedViewTransition.bind(null, lanes)
         : (null: any),
     );
   } else {
@@ -3712,15 +3708,13 @@ function suspendedViewTransition(reason: string): void {
   }
 }
 
-function finishedViewTransition(
-  lanes: Lanes,
-  task: null | ConsoleTask, // DEV-only
-): void {
+function finishedViewTransition(lanes: Lanes): void {
   if (enableProfilerTimer && enableComponentPerformanceTrack) {
     if ((animatingLanes & lanes) === NoLanes) {
       // Was already stopped by some other action or maybe other root.
       return;
     }
+    const task = animatingTask;
     stopAnimating(lanes);
     // If an affected track isn't in the middle of rendering or committing, log from the previous
     // finished render until the end of the animation.
@@ -3835,7 +3829,7 @@ function flushLayoutEffects(): void {
         commitEndTime, // The start is the end of the first commit part.
         commitStartTime, // The end is the start of the second commit part.
         suspendedViewTransitionReason,
-        workInProgressUpdateTask, // TODO: Use a ViewTransition Task and this is not safe to read in this phase.
+        animatingTask,
       );
     }
   }
@@ -3938,7 +3932,7 @@ function flushSpawnedWork(): void {
         startViewTransitionStartTime,
         commitEndTime,
         pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT,
-        workInProgressUpdateTask, // TODO: Use a ViewTransition Task.
+        animatingTask,
       );
       if (pendingDelayedCommitReason !== ABORTED_VIEW_TRANSITION_COMMIT) {
         pendingDelayedCommitReason = ANIMATION_STARTED_COMMIT;
@@ -4440,11 +4434,7 @@ function flushPassiveEffectsImpl() {
     passiveEffectStartTime = now();
     if (pendingDelayedCommitReason === ANIMATION_STARTED_COMMIT) {
       // The animation was started, so we've been animating since that happened.
-      logAnimatingPhase(
-        commitEndTime,
-        passiveEffectStartTime,
-        workInProgressUpdateTask, // TODO: Use a ViewTransition Task
-      );
+      logAnimatingPhase(commitEndTime, passiveEffectStartTime, animatingTask);
     } else {
       logPaintYieldPhase(
         commitEndTime,

filename: packages/react-reconciler/src/ReactProfilerTimer.js
patch: @@ -93,6 +93,7 @@ export let retryClampTime: number = -0;
 export let idleClampTime: number = -0;
 
 export let animatingLanes: Lanes = NoLanes;
+export let animatingTask: null | ConsoleTask = null; // First ViewTransition applying an Animation.
 
 export let yieldReason: SuspendedReason = (0: any);
 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 {
 
 export function startAnimating(lanes: Lanes): void {
   animatingLanes |= lanes;
+  animatingTask = null;
 }
 
 export function stopAnimating(lanes: Lanes): void {
   animatingLanes &= ~lanes;
+  animatingTask = null;
+}
+
+export function trackAnimatingTask(task: ConsoleTask): void {
+  if (animatingTask === null) {
+    animatingTask = task;
+  }
 }