From 0c813c528d78a7b1c831f758353416709630a104 Mon Sep 17 00:00:00 2001
From: Ruslan Lesiutin <28902667+hoxyq@users.noreply.github.com>
Date: Fri, 12 Sep 2025 11:34:41 +0100
Subject: [PATCH 1/2] [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.
---
.../src/ReactFiberClassComponent.js | 6 +-
.../react-reconciler/src/ReactFiberHooks.js | 10 +-
.../src/ReactFiberPerformanceTrack.js | 125 +++++++++++-------
.../src/ReactFiberReconciler.js | 4 +-
.../src/ReactFiberWorkLoop.js | 8 ++
.../src/ReactProfilerTimer.js | 21 ++-
6 files changed, 115 insertions(+), 59 deletions(-)
diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js
index 22a80d8dbe977..1fae90f07b1af 100644
--- a/packages/react-reconciler/src/ReactFiberClassComponent.js
+++ b/packages/react-reconciler/src/ReactFiberClassComponent.js
@@ -179,7 +179,7 @@ const classComponentUpdater = {
const root = enqueueUpdate(fiber, update, lane);
if (root !== null) {
- startUpdateTimerByLane(lane, 'this.setState()');
+ startUpdateTimerByLane(lane, 'this.setState()', fiber);
scheduleUpdateOnFiber(root, fiber, lane);
entangleTransitions(root, fiber, lane);
}
@@ -205,7 +205,7 @@ const classComponentUpdater = {
const root = enqueueUpdate(fiber, update, lane);
if (root !== null) {
- startUpdateTimerByLane(lane, 'this.replaceState()');
+ startUpdateTimerByLane(lane, 'this.replaceState()', fiber);
scheduleUpdateOnFiber(root, fiber, lane);
entangleTransitions(root, fiber, lane);
}
@@ -231,7 +231,7 @@ const classComponentUpdater = {
const root = enqueueUpdate(fiber, update, lane);
if (root !== null) {
- startUpdateTimerByLane(lane, 'this.forceUpdate()');
+ startUpdateTimerByLane(lane, 'this.forceUpdate()', fiber);
scheduleUpdateOnFiber(root, fiber, lane);
entangleTransitions(root, fiber, lane);
}
diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js
index ca62bf5a718a5..66a390ebb8150 100644
--- a/packages/react-reconciler/src/ReactFiberHooks.js
+++ b/packages/react-reconciler/src/ReactFiberHooks.js
@@ -1866,7 +1866,7 @@ function subscribeToStore(
// read from the store.
if (checkIfSnapshotChanged(inst)) {
// Force a re-render.
- startUpdateTimerByLane(SyncLane, 'updateSyncExternalStore()');
+ startUpdateTimerByLane(SyncLane, 'updateSyncExternalStore()', fiber);
forceStoreRerender(fiber);
}
};
@@ -3518,7 +3518,7 @@ function refreshCache(fiber: Fiber, seedKey: ?() => T, seedValue: T): void {
const refreshUpdate = createLegacyQueueUpdate(lane);
const root = enqueueLegacyQueueUpdate(provider, refreshUpdate, lane);
if (root !== null) {
- startUpdateTimerByLane(lane, 'refresh()');
+ startUpdateTimerByLane(lane, 'refresh()', fiber);
scheduleUpdateOnFiber(root, provider, lane);
entangleLegacyQueueTransitions(root, provider, lane);
}
@@ -3587,7 +3587,7 @@ function dispatchReducerAction(
} else {
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
- startUpdateTimerByLane(lane, 'dispatch()');
+ startUpdateTimerByLane(lane, 'dispatch()', fiber);
scheduleUpdateOnFiber(root, fiber, lane);
entangleTransitionUpdate(root, queue, lane);
}
@@ -3621,7 +3621,7 @@ function dispatchSetState(
lane,
);
if (didScheduleUpdate) {
- startUpdateTimerByLane(lane, 'setState()');
+ startUpdateTimerByLane(lane, 'setState()', fiber);
}
markUpdateInDevTools(fiber, lane, action);
}
@@ -3783,7 +3783,7 @@ function dispatchOptimisticSetState(
// will never be attempted before the optimistic update. This currently
// holds because the optimistic update is always synchronous. If we ever
// change that, we'll need to account for this.
- startUpdateTimerByLane(lane, 'setOptimistic()');
+ startUpdateTimerByLane(lane, 'setOptimistic()', fiber);
scheduleUpdateOnFiber(root, fiber, lane);
// Optimistic updates are always synchronous, so we don't need to call
// entangleTransitionUpdate here.
diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js
index 8e86a85476965..86f96d5d079aa 100644
--- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js
+++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js
@@ -631,6 +631,8 @@ export function logBlockingStart(
renderStartTime: number,
lanes: Lanes,
debugTask: null | ConsoleTask, // DEV-only
+ updateMethodName: null | string,
+ updateComponentName: null | string,
): void {
if (supportsUserTiming) {
currentTrack = 'Blocking';
@@ -672,34 +674,46 @@ export function logBlockingStart(
: includesOnlyHydrationOrOffscreenLanes(lanes)
? 'tertiary-light'
: 'primary-light';
- if (__DEV__ && debugTask) {
- debugTask.run(
- // $FlowFixMe[method-unbinding]
- console.timeStamp.bind(
- console,
- isPingedUpdate
- ? 'Promise Resolved'
- : isSpawnedUpdate
- ? 'Cascading Update'
- : renderStartTime - updateTime > 5
- ? 'Update Blocked'
- : 'Update',
- updateTime,
- renderStartTime,
- currentTrack,
- LANES_TRACK_GROUP,
- color,
- ),
- );
+ const label = isPingedUpdate
+ ? 'Promise Resolved'
+ : isSpawnedUpdate
+ ? 'Cascading Update'
+ : renderStartTime - updateTime > 5
+ ? 'Update Blocked'
+ : 'Update';
+
+ if (__DEV__) {
+ const properties = [];
+ if (updateComponentName != null) {
+ properties.push(['Component name', updateComponentName]);
+ }
+ if (updateMethodName != null) {
+ properties.push(['Method name', updateMethodName]);
+ }
+ const measureOptions = {
+ start: updateTime,
+ end: renderStartTime,
+ detail: {
+ devtools: {
+ properties,
+ track: currentTrack,
+ trackGroup: LANES_TRACK_GROUP,
+ color,
+ },
+ },
+ };
+
+ if (debugTask) {
+ debugTask.run(
+ // $FlowFixMe[method-unbinding]
+ performance.measure.bind(performance, label, measureOptions),
+ );
+ } else {
+ performance.measure(label, measureOptions);
+ }
} else {
console.timeStamp(
- isPingedUpdate
- ? 'Promise Resolved'
- : isSpawnedUpdate
- ? 'Cascading Update'
- : renderStartTime - updateTime > 5
- ? 'Update Blocked'
- : 'Update',
+ label,
updateTime,
renderStartTime,
currentTrack,
@@ -720,6 +734,8 @@ export function logTransitionStart(
isPingedUpdate: boolean,
renderStartTime: number,
debugTask: null | ConsoleTask, // DEV-only
+ updateMethodName: null | string,
+ updateComponentName: null | string,
): void {
if (supportsUserTiming) {
currentTrack = 'Transition';
@@ -781,30 +797,43 @@ export function logTransitionStart(
}
if (updateTime > 0 && renderStartTime > updateTime) {
// Log the time from when we called setState until we started rendering.
- if (__DEV__ && debugTask) {
- debugTask.run(
- // $FlowFixMe[method-unbinding]
- console.timeStamp.bind(
- console,
- isPingedUpdate
- ? 'Promise Resolved'
- : renderStartTime - updateTime > 5
- ? 'Update Blocked'
- : 'Update',
- updateTime,
- renderStartTime,
- currentTrack,
- LANES_TRACK_GROUP,
- 'primary-light',
- ),
- );
+ const label = isPingedUpdate
+ ? 'Promise Resolved'
+ : renderStartTime - updateTime > 5
+ ? 'Update Blocked'
+ : 'Update';
+ if (__DEV__) {
+ const properties = [];
+ if (updateComponentName != null) {
+ properties.push(['Component name', updateComponentName]);
+ }
+ if (updateMethodName != null) {
+ properties.push(['Method name', updateMethodName]);
+ }
+ const measureOptions = {
+ start: updateTime,
+ end: renderStartTime,
+ detail: {
+ devtools: {
+ properties,
+ track: currentTrack,
+ trackGroup: LANES_TRACK_GROUP,
+ color: 'primary-light',
+ },
+ },
+ };
+
+ if (debugTask) {
+ debugTask.run(
+ // $FlowFixMe[method-unbinding]
+ performance.measure.bind(performance, label, measureOptions),
+ );
+ } else {
+ performance.measure(label, measureOptions);
+ }
} else {
console.timeStamp(
- isPingedUpdate
- ? 'Promise Resolved'
- : renderStartTime - updateTime > 5
- ? 'Update Blocked'
- : 'Update',
+ label,
updateTime,
renderStartTime,
currentTrack,
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js
index 14764c0dcb7e9..3b4d321416b98 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.js
@@ -346,7 +346,7 @@ export function createHydrationContainer(
update.callback =
callback !== undefined && callback !== null ? callback : null;
enqueueUpdate(current, update, lane);
- startUpdateTimerByLane(lane, 'hydrateRoot()');
+ startUpdateTimerByLane(lane, 'hydrateRoot()', null);
scheduleInitialHydrationOnRoot(root, lane);
return root;
@@ -453,7 +453,7 @@ function updateContainerImpl(
const root = enqueueUpdate(rootFiber, update, lane);
if (root !== null) {
- startUpdateTimerByLane(lane, 'root.render()');
+ startUpdateTimerByLane(lane, 'root.render()', null);
scheduleUpdateOnFiber(root, rootFiber, lane);
entangleTransitions(root, rootFiber, lane);
}
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js
index d1e826797c8cb..00c8ce91c1a75 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js
@@ -267,6 +267,8 @@ import {
blockingUpdateTime,
blockingUpdateTask,
blockingUpdateType,
+ blockingUpdateMethodName,
+ blockingUpdateComponentName,
blockingEventTime,
blockingEventType,
blockingEventIsRepeat,
@@ -276,6 +278,8 @@ import {
transitionUpdateTime,
transitionUpdateTask,
transitionUpdateType,
+ transitionUpdateMethodName,
+ transitionUpdateComponentName,
transitionEventTime,
transitionEventType,
transitionEventIsRepeat,
@@ -1940,6 +1944,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
renderStartTime,
lanes,
blockingUpdateTask,
+ blockingUpdateMethodName,
+ blockingUpdateComponentName,
);
clearBlockingTimers();
}
@@ -1980,6 +1986,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
transitionUpdateType === PINGED_UPDATE,
renderStartTime,
transitionUpdateTask,
+ transitionUpdateMethodName,
+ transitionUpdateComponentName,
);
clearTransitionTimers();
}
diff --git a/packages/react-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js
index 4768ffffb5635..a1779235fc02b 100644
--- a/packages/react-reconciler/src/ReactProfilerTimer.js
+++ b/packages/react-reconciler/src/ReactProfilerTimer.js
@@ -33,6 +33,7 @@ import {
enableComponentPerformanceTrack,
} from 'shared/ReactFeatureFlags';
+import getComponentNameFromFiber from './getComponentNameFromFiber';
import {isAlreadyRendering} from './ReactFiberWorkLoop';
// Intentionally not named imports because Rollup would use dynamic dispatch for
@@ -68,6 +69,8 @@ export let blockingClampTime: number = -0;
export let blockingUpdateTime: number = -1.1; // First sync setState scheduled.
export let blockingUpdateTask: null | ConsoleTask = null; // First sync setState's stack trace.
export let blockingUpdateType: UpdateType = 0;
+export let blockingUpdateMethodName: null | string = null; // The name of the method that caused first sync update.
+export let blockingUpdateComponentName: null | string = null; // The name of the component where first sync update happened.
export let blockingEventTime: number = -1.1; // Event timeStamp of the first setState.
export let blockingEventType: null | string = null; // Event type of the first setState.
export let blockingEventIsRepeat: boolean = false;
@@ -78,6 +81,8 @@ export let transitionStartTime: number = -1.1; // First startTransition call bef
export let transitionUpdateTime: number = -1.1; // First transition setState scheduled.
export let transitionUpdateType: UpdateType = 0;
export let transitionUpdateTask: null | ConsoleTask = null; // First transition setState's stack trace.
+export let transitionUpdateMethodName: null | string = null; // The name of the method that caused first transition update.
+export let transitionUpdateComponentName: null | string = null; // The name of the component where first transition update happened.
export let transitionEventTime: number = -1.1; // Event timeStamp of the first transition.
export let transitionEventType: null | string = null; // Event type of the first transition.
export let transitionEventIsRepeat: boolean = false;
@@ -94,7 +99,11 @@ export function startYieldTimer(reason: SuspendedReason) {
yieldReason = reason;
}
-export function startUpdateTimerByLane(lane: Lane, method: string): void {
+export function startUpdateTimerByLane(
+ lane: Lane,
+ method: string,
+ fiber: Fiber | null,
+): void {
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
return;
}
@@ -102,6 +111,10 @@ export function startUpdateTimerByLane(lane: Lane, method: string): void {
if (blockingUpdateTime < 0) {
blockingUpdateTime = now();
blockingUpdateTask = createTask(method);
+ blockingUpdateMethodName = method;
+ if (__DEV__ && fiber != null) {
+ blockingUpdateComponentName = getComponentNameFromFiber(fiber);
+ }
if (isAlreadyRendering()) {
blockingUpdateType = SPAWNED_UPDATE;
}
@@ -125,6 +138,10 @@ export function startUpdateTimerByLane(lane: Lane, method: string): void {
if (transitionUpdateTime < 0) {
transitionUpdateTime = now();
transitionUpdateTask = createTask(method);
+ transitionUpdateMethodName = method;
+ if (__DEV__ && fiber != null) {
+ transitionUpdateComponentName = getComponentNameFromFiber(fiber);
+ }
if (transitionStartTime < 0) {
const newEventTime = resolveEventTimeStamp();
const newEventType = resolveEventType();
@@ -225,6 +242,8 @@ export function trackSuspendedTime(lanes: Lanes, renderEndTime: number) {
export function clearBlockingTimers(): void {
blockingUpdateTime = -1.1;
blockingUpdateType = 0;
+ blockingUpdateMethodName = null;
+ blockingUpdateComponentName = null;
blockingSuspendedTime = -1.1;
blockingEventIsRepeat = true;
}
From 0e10ee906e3ea55e4d717d4db498e1159235b06b Mon Sep 17 00:00:00 2001
From: Ruslan Lesiutin <28902667+hoxyq@users.noreply.github.com>
Date: Fri, 12 Sep 2025 12:20:39 +0100
Subject: [PATCH 2/2] [Reconciler] Set ProfileMode for Host Root Fiber by
default in dev (#34432)
Requiring DevTools to be present for dev builds seems like an overkill,
let's enable the instrumentation by default.
Nothing changes for profiling or production artifacts.
---
packages/react-reconciler/src/ReactFiber.js | 8 ++++----
.../react/src/__tests__/ReactProfiler-test.internal.js | 1 +
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js
index 0af7ffb767bfb..fb2c7347010b6 100644
--- a/packages/react-reconciler/src/ReactFiber.js
+++ b/packages/react-reconciler/src/ReactFiber.js
@@ -532,10 +532,10 @@ export function createHostRootFiber(
mode = NoMode;
}
- if (enableProfilerTimer && isDevToolsPresent) {
- // Always collect profile timings when DevTools are present.
- // This enables DevTools to start capturing timing at any point–
- // Without some nodes in the tree having empty base times.
+ if (__DEV__ || (enableProfilerTimer && isDevToolsPresent)) {
+ // dev: Enable profiling instrumentation by default.
+ // profile: enabled if DevTools is present or subtree is wrapped in .
+ // production: disabled.
mode |= ProfileMode;
}
diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js
index d51b037c18e4c..a8c00b2b562dc 100644
--- a/packages/react/src/__tests__/ReactProfiler-test.internal.js
+++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js
@@ -125,6 +125,7 @@ describe(`onRender`, () => {
expect(callback).toHaveBeenCalledTimes(1);
});
+ // @gate !__DEV__
it('does not record times for components outside of Profiler tree', async () => {
// Mock the Scheduler module so we can track how many times the current
// time is read