@samitouri / QOS-React / commits / 0c813c528d

[Tracks]: display method name and component name for updates in DEV (#34463)

For every "Update" entry we are going to add properties that will be displayed when the user clicks on that entry: name of the method that caused this first update and name of the component where this update happened. We could use the name of the component as a deeplink to React DevTools components panel in the future, once we support stable identificators on Fibers. <img width="1444" height="530" alt="Screenshot 2025-09-10 at 18 31 10" src="https://github.com/user-attachments/assets/7f9af037-2e7f-4e7b-9b7e-bf9f7d5a6e72" /> <img width="2088" height="530" alt="Screenshot 2025-09-10 at 18 24 21" src="https://github.com/user-attachments/assets/f557a173-bd9b-43f7-9333-74066f433ced" /> <img width="2088" height="530" alt="Screenshot 2025-09-10 at 18 26 04" src="https://github.com/user-attachments/assets/ff37d13f-bbe3-4f85-800e-81aa3aed7833" />

Ruslan Lesiutin committed Sep 12, 2025 at 11:34 UTC 0c813c528d78a7b1c831f758353416709630a104
6 files changed +115 -59
packages/react-reconciler/src/ReactFiberClassComponent.js
+3 -3
@@ -179,7 +179,7 @@ const classComponentUpdater = {
179
180 const root = enqueueUpdate(fiber, update, lane);
181 if (root !== null) {
182 - startUpdateTimerByLane(lane, 'this.setState()');
182 + startUpdateTimerByLane(lane, 'this.setState()', fiber);
183 scheduleUpdateOnFiber(root, fiber, lane);
184 entangleTransitions(root, fiber, lane);
185 }
@@ -205,7 +205,7 @@ const classComponentUpdater = {
205
206 const root = enqueueUpdate(fiber, update, lane);
207 if (root !== null) {
208 - startUpdateTimerByLane(lane, 'this.replaceState()');
208 + startUpdateTimerByLane(lane, 'this.replaceState()', fiber);
209 scheduleUpdateOnFiber(root, fiber, lane);
210 entangleTransitions(root, fiber, lane);
211 }
@@ -231,7 +231,7 @@ const classComponentUpdater = {
231
232 const root = enqueueUpdate(fiber, update, lane);
233 if (root !== null) {
234 - startUpdateTimerByLane(lane, 'this.forceUpdate()');
234 + startUpdateTimerByLane(lane, 'this.forceUpdate()', fiber);
235 scheduleUpdateOnFiber(root, fiber, lane);
236 entangleTransitions(root, fiber, lane);
237 }
packages/react-reconciler/src/ReactFiberHooks.js
+5 -5
@@ -1866,7 +1866,7 @@ function subscribeToStore<T>(
1866 // read from the store.
1867 if (checkIfSnapshotChanged(inst)) {
1868 // Force a re-render.
1869 - startUpdateTimerByLane(SyncLane, 'updateSyncExternalStore()');
1869 + startUpdateTimerByLane(SyncLane, 'updateSyncExternalStore()', fiber);
1870 forceStoreRerender(fiber);
1871 }
1872 };
@@ -3518,7 +3518,7 @@ function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T): void {
3518 const refreshUpdate = createLegacyQueueUpdate(lane);
3519 const root = enqueueLegacyQueueUpdate(provider, refreshUpdate, lane);
3520 if (root !== null) {
3521 - startUpdateTimerByLane(lane, 'refresh()');
3521 + startUpdateTimerByLane(lane, 'refresh()', fiber);
3522 scheduleUpdateOnFiber(root, provider, lane);
3523 entangleLegacyQueueTransitions(root, provider, lane);
3524 }
@@ -3587,7 +3587,7 @@ function dispatchReducerAction<S, A>(
3587 } else {
3588 const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
3589 if (root !== null) {
3590 - startUpdateTimerByLane(lane, 'dispatch()');
3590 + startUpdateTimerByLane(lane, 'dispatch()', fiber);
3591 scheduleUpdateOnFiber(root, fiber, lane);
3592 entangleTransitionUpdate(root, queue, lane);
3593 }
@@ -3621,7 +3621,7 @@ function dispatchSetState<S, A>(
3621 lane,
3622 );
3623 if (didScheduleUpdate) {
3624 - startUpdateTimerByLane(lane, 'setState()');
3624 + startUpdateTimerByLane(lane, 'setState()', fiber);
3625 }
3626 markUpdateInDevTools(fiber, lane, action);
3627 }
@@ -3783,7 +3783,7 @@ function dispatchOptimisticSetState<S, A>(
3783 // will never be attempted before the optimistic update. This currently
3784 // holds because the optimistic update is always synchronous. If we ever
3785 // change that, we'll need to account for this.
3786 - startUpdateTimerByLane(lane, 'setOptimistic()');
3786 + startUpdateTimerByLane(lane, 'setOptimistic()', fiber);
3787 scheduleUpdateOnFiber(root, fiber, lane);
3788 // Optimistic updates are always synchronous, so we don't need to call
3789 // entangleTransitionUpdate here.
packages/react-reconciler/src/ReactFiberPerformanceTrack.js
+77 -48
@@ -631,6 +631,8 @@ export function logBlockingStart(
631 renderStartTime: number,
632 lanes: Lanes,
633 debugTask: null | ConsoleTask, // DEV-only
634 + updateMethodName: null | string,
635 + updateComponentName: null | string,
636 ): void {
637 if (supportsUserTiming) {
638 currentTrack = 'Blocking';
@@ -672,34 +674,46 @@ export function logBlockingStart(
674 : includesOnlyHydrationOrOffscreenLanes(lanes)
675 ? 'tertiary-light'
676 : 'primary-light';
675 - if (__DEV__ && debugTask) {
676 - debugTask.run(
677 - // $FlowFixMe[method-unbinding]
678 - console.timeStamp.bind(
679 - console,
680 - isPingedUpdate
681 - ? 'Promise Resolved'
682 - : isSpawnedUpdate
683 - ? 'Cascading Update'
684 - : renderStartTime - updateTime > 5
685 - ? 'Update Blocked'
686 - : 'Update',
687 - updateTime,
688 - renderStartTime,
689 - currentTrack,
690 - LANES_TRACK_GROUP,
691 - color,
692 - ),
693 - );
677 + const label = isPingedUpdate
678 + ? 'Promise Resolved'
679 + : isSpawnedUpdate
680 + ? 'Cascading Update'
681 + : renderStartTime - updateTime > 5
682 + ? 'Update Blocked'
683 + : 'Update';
684 +
685 + if (__DEV__) {
686 + const properties = [];
687 + if (updateComponentName != null) {
688 + properties.push(['Component name', updateComponentName]);
689 + }
690 + if (updateMethodName != null) {
691 + properties.push(['Method name', updateMethodName]);
692 + }
693 + const measureOptions = {
694 + start: updateTime,
695 + end: renderStartTime,
696 + detail: {
697 + devtools: {
698 + properties,
699 + track: currentTrack,
700 + trackGroup: LANES_TRACK_GROUP,
701 + color,
702 + },
703 + },
704 + };
705 +
706 + if (debugTask) {
707 + debugTask.run(
708 + // $FlowFixMe[method-unbinding]
709 + performance.measure.bind(performance, label, measureOptions),
710 + );
711 + } else {
712 + performance.measure(label, measureOptions);
713 + }
714 } else {
715 console.timeStamp(
696 - isPingedUpdate
697 - ? 'Promise Resolved'
698 - : isSpawnedUpdate
699 - ? 'Cascading Update'
700 - : renderStartTime - updateTime > 5
701 - ? 'Update Blocked'
702 - : 'Update',
716 + label,
717 updateTime,
718 renderStartTime,
719 currentTrack,
@@ -720,6 +734,8 @@ export function logTransitionStart(
734 isPingedUpdate: boolean,
735 renderStartTime: number,
736 debugTask: null | ConsoleTask, // DEV-only
737 + updateMethodName: null | string,
738 + updateComponentName: null | string,
739 ): void {
740 if (supportsUserTiming) {
741 currentTrack = 'Transition';
@@ -781,30 +797,43 @@ export function logTransitionStart(
797 }
798 if (updateTime > 0 && renderStartTime > updateTime) {
799 // Log the time from when we called setState until we started rendering.
784 - if (__DEV__ && debugTask) {
785 - debugTask.run(
786 - // $FlowFixMe[method-unbinding]
787 - console.timeStamp.bind(
788 - console,
789 - isPingedUpdate
790 - ? 'Promise Resolved'
791 - : renderStartTime - updateTime > 5
792 - ? 'Update Blocked'
793 - : 'Update',
794 - updateTime,
795 - renderStartTime,
796 - currentTrack,
797 - LANES_TRACK_GROUP,
798 - 'primary-light',
799 - ),
800 - );
800 + const label = isPingedUpdate
801 + ? 'Promise Resolved'
802 + : renderStartTime - updateTime > 5
803 + ? 'Update Blocked'
804 + : 'Update';
805 + if (__DEV__) {
806 + const properties = [];
807 + if (updateComponentName != null) {
808 + properties.push(['Component name', updateComponentName]);
809 + }
810 + if (updateMethodName != null) {
811 + properties.push(['Method name', updateMethodName]);
812 + }
813 + const measureOptions = {
814 + start: updateTime,
815 + end: renderStartTime,
816 + detail: {
817 + devtools: {
818 + properties,
819 + track: currentTrack,
820 + trackGroup: LANES_TRACK_GROUP,
821 + color: 'primary-light',
822 + },
823 + },
824 + };
825 +
826 + if (debugTask) {
827 + debugTask.run(
828 + // $FlowFixMe[method-unbinding]
829 + performance.measure.bind(performance, label, measureOptions),
830 + );
831 + } else {
832 + performance.measure(label, measureOptions);
833 + }
834 } else {
835 console.timeStamp(
803 - isPingedUpdate
804 - ? 'Promise Resolved'
805 - : renderStartTime - updateTime > 5
806 - ? 'Update Blocked'
807 - : 'Update',
836 + label,
837 updateTime,
838 renderStartTime,
839 currentTrack,
packages/react-reconciler/src/ReactFiberReconciler.js
+2 -2
@@ -346,7 +346,7 @@ export function createHydrationContainer(
346 update.callback =
347 callback !== undefined && callback !== null ? callback : null;
348 enqueueUpdate(current, update, lane);
349 - startUpdateTimerByLane(lane, 'hydrateRoot()');
349 + startUpdateTimerByLane(lane, 'hydrateRoot()', null);
350 scheduleInitialHydrationOnRoot(root, lane);
351
352 return root;
@@ -453,7 +453,7 @@ function updateContainerImpl(
453
454 const root = enqueueUpdate(rootFiber, update, lane);
455 if (root !== null) {
456 - startUpdateTimerByLane(lane, 'root.render()');
456 + startUpdateTimerByLane(lane, 'root.render()', null);
457 scheduleUpdateOnFiber(root, rootFiber, lane);
458 entangleTransitions(root, rootFiber, lane);
459 }
packages/react-reconciler/src/ReactFiberWorkLoop.js
+8
@@ -267,6 +267,8 @@ import {
267 blockingUpdateTime,
268 blockingUpdateTask,
269 blockingUpdateType,
270 + blockingUpdateMethodName,
271 + blockingUpdateComponentName,
272 blockingEventTime,
273 blockingEventType,
274 blockingEventIsRepeat,
@@ -276,6 +278,8 @@ import {
278 transitionUpdateTime,
279 transitionUpdateTask,
280 transitionUpdateType,
281 + transitionUpdateMethodName,
282 + transitionUpdateComponentName,
283 transitionEventTime,
284 transitionEventType,
285 transitionEventIsRepeat,
@@ -1940,6 +1944,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1944 renderStartTime,
1945 lanes,
1946 blockingUpdateTask,
1947 + blockingUpdateMethodName,
1948 + blockingUpdateComponentName,
1949 );
1950 clearBlockingTimers();
1951 }
@@ -1980,6 +1986,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1986 transitionUpdateType === PINGED_UPDATE,
1987 renderStartTime,
1988 transitionUpdateTask,
1989 + transitionUpdateMethodName,
1990 + transitionUpdateComponentName,
1991 );
1992 clearTransitionTimers();
1993 }
packages/react-reconciler/src/ReactProfilerTimer.js
+20 -1
@@ -33,6 +33,7 @@ import {
33 enableComponentPerformanceTrack,
34 } from 'shared/ReactFeatureFlags';
35
36 +import getComponentNameFromFiber from './getComponentNameFromFiber';
37 import {isAlreadyRendering} from './ReactFiberWorkLoop';
38
39 // Intentionally not named imports because Rollup would use dynamic dispatch for
@@ -68,6 +69,8 @@ export let blockingClampTime: number = -0;
69 export let blockingUpdateTime: number = -1.1; // First sync setState scheduled.
70 export let blockingUpdateTask: null | ConsoleTask = null; // First sync setState's stack trace.
71 export let blockingUpdateType: UpdateType = 0;
72 +export let blockingUpdateMethodName: null | string = null; // The name of the method that caused first sync update.
73 +export let blockingUpdateComponentName: null | string = null; // The name of the component where first sync update happened.
74 export let blockingEventTime: number = -1.1; // Event timeStamp of the first setState.
75 export let blockingEventType: null | string = null; // Event type of the first setState.
76 export let blockingEventIsRepeat: boolean = false;
@@ -78,6 +81,8 @@ export let transitionStartTime: number = -1.1; // First startTransition call bef
81 export let transitionUpdateTime: number = -1.1; // First transition setState scheduled.
82 export let transitionUpdateType: UpdateType = 0;
83 export let transitionUpdateTask: null | ConsoleTask = null; // First transition setState's stack trace.
84 +export let transitionUpdateMethodName: null | string = null; // The name of the method that caused first transition update.
85 +export let transitionUpdateComponentName: null | string = null; // The name of the component where first transition update happened.
86 export let transitionEventTime: number = -1.1; // Event timeStamp of the first transition.
87 export let transitionEventType: null | string = null; // Event type of the first transition.
88 export let transitionEventIsRepeat: boolean = false;
@@ -94,7 +99,11 @@ export function startYieldTimer(reason: SuspendedReason) {
99 yieldReason = reason;
100 }
101
97 -export function startUpdateTimerByLane(lane: Lane, method: string): void {
102 +export function startUpdateTimerByLane(
103 + lane: Lane,
104 + method: string,
105 + fiber: Fiber | null,
106 +): void {
107 if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
108 return;
109 }
@@ -102,6 +111,10 @@ export function startUpdateTimerByLane(lane: Lane, method: string): void {
111 if (blockingUpdateTime < 0) {
112 blockingUpdateTime = now();
113 blockingUpdateTask = createTask(method);
114 + blockingUpdateMethodName = method;
115 + if (__DEV__ && fiber != null) {
116 + blockingUpdateComponentName = getComponentNameFromFiber(fiber);
117 + }
118 if (isAlreadyRendering()) {
119 blockingUpdateType = SPAWNED_UPDATE;
120 }
@@ -125,6 +138,10 @@ export function startUpdateTimerByLane(lane: Lane, method: string): void {
138 if (transitionUpdateTime < 0) {
139 transitionUpdateTime = now();
140 transitionUpdateTask = createTask(method);
141 + transitionUpdateMethodName = method;
142 + if (__DEV__ && fiber != null) {
143 + transitionUpdateComponentName = getComponentNameFromFiber(fiber);
144 + }
145 if (transitionStartTime < 0) {
146 const newEventTime = resolveEventTimeStamp();
147 const newEventType = resolveEventType();
@@ -225,6 +242,8 @@ export function trackSuspendedTime(lanes: Lanes, renderEndTime: number) {
242 export function clearBlockingTimers(): void {
243 blockingUpdateTime = -1.1;
244 blockingUpdateType = 0;
245 + blockingUpdateMethodName = null;
246 + blockingUpdateComponentName = null;
247 blockingSuspendedTime = -1.1;
248 blockingEventIsRepeat = true;
249 }