[Flight] Promote enableAsyncDebugInfo to stable without enableComponentPerformanceTrack (#33996)
There's a lot of overlap between `enableComponentPerformanceTrack` and `enableAsyncDebugInfo` because they both rely on timing information. The former is mainly emit timestamps for how long server components and awaits took. The latter how long I/O took. `enableAsyncDebugInfo` is currently primarily for the component performance track but its meta data is useful for other debug tools too. This promotes that flag to stable. However, `enableComponentPerformanceTrack` needs more work due to performance concerns with Chrome DevTools so I need to separate them. This keeps doing most of the timing tracking on the server but doesn't emit the per-server component time stamps when `enableComponentPerformanceTrack` is false.
Sebastian Markbåge committed
Jul 25, 2025 at 04:59 UTC
99be14c883c5c83c9a087d37e19d93d6afb839ed
4 files changed
+83
-50
packages/react-client/src/ReactFlightClient.js
+6
-7
@@ -3647,7 +3647,7 @@ function initializeIOInfo(response: Response, ioInfo: ReactIOInfo): void {
3647
// $FlowFixMe[cannot-write]
3648
ioInfo.end += response._timeOrigin;
3649
3650
- if (response._replayConsole) {
3650
+ if (enableComponentPerformanceTrack && response._replayConsole) {
3651
const env = response._rootEnvironmentName;
3652
const promise = ioInfo.value;
3653
if (promise) {
@@ -4149,7 +4149,10 @@ function processFullStringRow(
4149
return;
4150
}
4151
case 78 /* "N" */: {
4152
- if (enableProfilerTimer && enableComponentPerformanceTrack) {
4152
+ if (
4153
+ enableProfilerTimer &&
4154
+ (enableComponentPerformanceTrack || enableAsyncDebugInfo)
4155
+ ) {
4156
// Track the time origin for future debug info. We track it relative
4157
// to the current environment's time space.
4158
const timeOrigin: number = +row;
@@ -4169,11 +4172,7 @@ function processFullStringRow(
4172
// Fallthrough to share the error with Console entries.
4173
}
4174
case 74 /* "J" */: {
4172
- if (
4173
- enableProfilerTimer &&
4174
- enableComponentPerformanceTrack &&
4175
- enableAsyncDebugInfo
4176
- ) {
4175
+ if (enableProfilerTimer && enableAsyncDebugInfo) {
4176
resolveIOInfo(response, id, row);
4177
return;
4178
}
packages/react-client/src/__tests__/ReactFlight-test.js
+2
-2
@@ -2898,7 +2898,7 @@ describe('ReactFlight', () => {
2898
);
2899
});
2900
2901
- // @gate enableAsyncIterableChildren
2901
+ // @gate enableAsyncIterableChildren && enableComponentPerformanceTrack
2902
it('preserves debug info for server-to-server pass through of async iterables', async () => {
2903
let resolve;
2904
const iteratorPromise = new Promise(r => (resolve = r));
@@ -3727,7 +3727,7 @@ describe('ReactFlight', () => {
3727
expect(caughtError.digest).toBe('digest("my-error")');
3728
});
3729
3730
- // @gate __DEV__ && enableComponentPerformanceTrack
3730
+ // @gate __DEV__ && enableComponentPerformanceTrack
3731
it('can render deep but cut off JSX in debug info', async () => {
3732
function createDeepJSX(n) {
3733
if (n <= 0) {
packages/react-server/src/ReactFlightServer.js
+74
-40
@@ -731,7 +731,10 @@ function RequestInstance(
731
}
732
733
let timeOrigin: number;
734
- if (enableProfilerTimer && enableComponentPerformanceTrack) {
734
+ if (
735
+ enableProfilerTimer &&
736
+ (enableComponentPerformanceTrack || enableAsyncDebugInfo)
737
+ ) {
738
// We start by serializing the time origin. Any future timestamps will be
739
// emitted relatively to this origin. Instead of using performance.timeOrigin
740
// as this origin, we use the timestamp at the start of the request.
@@ -978,7 +981,10 @@ function serializeThenable(
981
task.keyPath, // the server component sequence continues through Promise-as-a-child.
982
task.implicitSlot,
983
request.abortableTasks,
981
- enableProfilerTimer && enableComponentPerformanceTrack ? task.time : 0,
984
+ enableProfilerTimer &&
985
+ (enableComponentPerformanceTrack || enableAsyncDebugInfo)
986
+ ? task.time
987
+ : 0,
988
__DEV__ ? task.debugOwner : null,
989
__DEV__ ? task.debugStack : null,
990
__DEV__ ? task.debugTask : null,
@@ -1048,7 +1054,10 @@ function serializeThenable(
1054
},
1055
reason => {
1056
if (newTask.status === PENDING) {
1051
- if (enableProfilerTimer && enableComponentPerformanceTrack) {
1057
+ if (
1058
+ enableProfilerTimer &&
1059
+ (enableComponentPerformanceTrack || enableAsyncDebugInfo)
1060
+ ) {
1061
// If this is async we need to time when this task finishes.
1062
newTask.timed = true;
1063
}
@@ -1094,7 +1103,10 @@ function serializeReadableStream(
1103
task.keyPath,
1104
task.implicitSlot,
1105
request.abortableTasks,
1097
- enableProfilerTimer && enableComponentPerformanceTrack ? task.time : 0,
1106
+ enableProfilerTimer &&
1107
+ (enableComponentPerformanceTrack || enableAsyncDebugInfo)
1108
+ ? task.time
1109
+ : 0,
1110
__DEV__ ? task.debugOwner : null,
1111
__DEV__ ? task.debugStack : null,
1112
__DEV__ ? task.debugTask : null,
@@ -1186,7 +1198,10 @@ function serializeAsyncIterable(
1198
task.keyPath,
1199
task.implicitSlot,
1200
request.abortableTasks,
1189
- enableProfilerTimer && enableComponentPerformanceTrack ? task.time : 0,
1201
+ enableProfilerTimer &&
1202
+ (enableComponentPerformanceTrack || enableAsyncDebugInfo)
1203
+ ? task.time
1204
+ : 0,
1205
__DEV__ ? task.debugOwner : null,
1206
__DEV__ ? task.debugStack : null,
1207
__DEV__ ? task.debugTask : null,
@@ -1616,7 +1631,10 @@ function renderFunctionComponent<Props>(
1631
outlineComponentInfo(request, componentDebugInfo);
1632
1633
// Track when we started rendering this component.
1619
- if (enableProfilerTimer && enableComponentPerformanceTrack) {
1634
+ if (
1635
+ enableProfilerTimer &&
1636
+ (enableComponentPerformanceTrack || enableAsyncDebugInfo)
1637
+ ) {
1638
advanceTaskTime(request, task, performance.now());
1639
}
1640
@@ -1686,12 +1704,7 @@ function renderFunctionComponent<Props>(
1704
throw null;
1705
}
1706
1689
- if (
1690
- __DEV__ ||
1691
- (enableProfilerTimer &&
1692
- enableComponentPerformanceTrack &&
1693
- enableAsyncDebugInfo)
1694
- ) {
1707
+ if (__DEV__ || (enableProfilerTimer && enableAsyncDebugInfo)) {
1708
// Forward any debug information for any Promises that we use():ed during the render.
1709
// We do this at the end so that we don't keep doing this for each retry.
1710
const trackedThenables = getTrackedThenablesAfterRendering();
@@ -2016,7 +2029,10 @@ function deferTask(request: Request, task: Task): ReactJSONValue {
2029
task.keyPath, // unlike outlineModel this one carries along context
2030
task.implicitSlot,
2031
request.abortableTasks,
2019
- enableProfilerTimer && enableComponentPerformanceTrack ? task.time : 0,
2032
+ enableProfilerTimer &&
2033
+ (enableComponentPerformanceTrack || enableAsyncDebugInfo)
2034
+ ? task.time
2035
+ : 0,
2036
__DEV__ ? task.debugOwner : null,
2037
__DEV__ ? task.debugStack : null,
2038
__DEV__ ? task.debugTask : null,
@@ -2033,7 +2049,10 @@ function outlineTask(request: Request, task: Task): ReactJSONValue {
2049
task.keyPath, // unlike outlineModel this one carries along context
2050
task.implicitSlot,
2051
request.abortableTasks,
2036
- enableProfilerTimer && enableComponentPerformanceTrack ? task.time : 0,
2052
+ enableProfilerTimer &&
2053
+ (enableComponentPerformanceTrack || enableAsyncDebugInfo)
2054
+ ? task.time
2055
+ : 0,
2056
__DEV__ ? task.debugOwner : null,
2057
__DEV__ ? task.debugStack : null,
2058
__DEV__ ? task.debugTask : null,
@@ -2482,7 +2501,10 @@ function emitAsyncSequence(
2501
}
2502
2503
function pingTask(request: Request, task: Task): void {
2485
- if (enableProfilerTimer && enableComponentPerformanceTrack) {
2504
+ if (
2505
+ enableProfilerTimer &&
2506
+ (enableComponentPerformanceTrack || enableAsyncDebugInfo)
2507
+ ) {
2508
// If this was async we need to emit the time when it completes.
2509
task.timed = true;
2510
}
@@ -2587,7 +2609,10 @@ function createTask(
2609
| 'debugStack'
2610
| 'debugTask',
2611
>): any);
2590
- if (enableProfilerTimer && enableComponentPerformanceTrack) {
2612
+ if (
2613
+ enableProfilerTimer &&
2614
+ (enableComponentPerformanceTrack || enableAsyncDebugInfo)
2615
+ ) {
2616
task.timed = false;
2617
task.time = lastTimestamp;
2618
}
@@ -2795,7 +2820,8 @@ function outlineModel(request: Request, value: ReactClientValue): number {
2820
null, // The way we use outlining is for reusing an object.
2821
false, // It makes no sense for that use case to be contextual.
2822
request.abortableTasks,
2798
- enableProfilerTimer && enableComponentPerformanceTrack
2823
+ enableProfilerTimer &&
2824
+ (enableComponentPerformanceTrack || enableAsyncDebugInfo)
2825
? performance.now() // TODO: This should really inherit the time from the task.
2826
: 0,
2827
null, // TODO: Currently we don't associate any debug information with
@@ -3041,7 +3067,8 @@ function serializeBlob(request: Request, blob: Blob): string {
3067
null,
3068
false,
3069
request.abortableTasks,
3044
- enableProfilerTimer && enableComponentPerformanceTrack
3070
+ enableProfilerTimer &&
3071
+ (enableComponentPerformanceTrack || enableAsyncDebugInfo)
3072
? performance.now() // TODO: This should really inherit the time from the task.
3073
: 0,
3074
null, // TODO: Currently we don't associate any debug information with
@@ -3177,7 +3204,8 @@ function renderModel(
3204
task.keyPath,
3205
task.implicitSlot,
3206
request.abortableTasks,
3180
- enableProfilerTimer && enableComponentPerformanceTrack
3207
+ enableProfilerTimer &&
3208
+ (enableComponentPerformanceTrack || enableAsyncDebugInfo)
3209
? task.time
3210
: 0,
3211
__DEV__ ? task.debugOwner : null,
@@ -5130,11 +5158,7 @@ function forwardDebugInfoFromThenable(
5158
forwardDebugInfo(request, task, debugInfo);
5159
}
5160
}
5133
- if (
5134
- enableProfilerTimer &&
5135
- enableComponentPerformanceTrack &&
5136
- enableAsyncDebugInfo
5137
- ) {
5161
+ if (enableProfilerTimer && enableAsyncDebugInfo) {
5162
const sequence = getAsyncSequenceFromPromise(thenable);
5163
if (sequence !== null) {
5164
emitAsyncSequence(request, task, sequence, debugInfo, owner, stack);
@@ -5155,11 +5179,7 @@ function forwardDebugInfoFromCurrentContext(
5179
forwardDebugInfo(request, task, debugInfo);
5180
}
5181
}
5158
- if (
5159
- enableProfilerTimer &&
5160
- enableComponentPerformanceTrack &&
5161
- enableAsyncDebugInfo
5162
- ) {
5182
+ if (enableProfilerTimer && enableAsyncDebugInfo) {
5183
const sequence = getCurrentAsyncSequence();
5184
if (sequence !== null) {
5185
emitAsyncSequence(request, task, sequence, debugInfo, null, null);
@@ -5182,11 +5202,7 @@ function forwardDebugInfoFromAbortedTask(request: Request, task: Task): void {
5202
forwardDebugInfo(request, task, debugInfo);
5203
}
5204
}
5185
- if (
5186
- enableProfilerTimer &&
5187
- enableComponentPerformanceTrack &&
5188
- enableAsyncDebugInfo
5189
- ) {
5205
+ if (enableProfilerTimer && enableAsyncDebugInfo) {
5206
let thenable: null | Thenable<any> = null;
5207
if (typeof model.then === 'function') {
5208
thenable = (model: any);
@@ -5262,7 +5278,10 @@ function advanceTaskTime(
5278
task: Task,
5279
timestamp: number,
5280
): void {
5265
- if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
5281
+ if (
5282
+ !enableProfilerTimer ||
5283
+ (!enableComponentPerformanceTrack && !enableAsyncDebugInfo)
5284
+ ) {
5285
return;
5286
}
5287
// Emits a timing chunk, if the new timestamp is higher than the previous timestamp of this task.
@@ -5278,7 +5297,10 @@ function advanceTaskTime(
5297
}
5298
5299
function markOperationEndTime(request: Request, task: Task, timestamp: number) {
5281
- if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
5300
+ if (
5301
+ !enableProfilerTimer ||
5302
+ (!enableComponentPerformanceTrack && !enableAsyncDebugInfo)
5303
+ ) {
5304
return;
5305
}
5306
// This is like advanceTaskTime() but always emits a timing chunk even if it doesn't advance.
@@ -5384,7 +5406,10 @@ function emitChunk(
5406
}
5407
5408
function erroredTask(request: Request, task: Task, error: mixed): void {
5387
- if (enableProfilerTimer && enableComponentPerformanceTrack) {
5409
+ if (
5410
+ enableProfilerTimer &&
5411
+ (enableComponentPerformanceTrack || enableAsyncDebugInfo)
5412
+ ) {
5413
if (task.timed) {
5414
markOperationEndTime(request, task, performance.now());
5415
}
@@ -5467,7 +5492,10 @@ function retryTask(request: Request, task: Task): void {
5492
}
5493
}
5494
// We've finished rendering. Log the end time.
5470
- if (enableProfilerTimer && enableComponentPerformanceTrack) {
5495
+ if (
5496
+ enableProfilerTimer &&
5497
+ (enableComponentPerformanceTrack || enableAsyncDebugInfo)
5498
+ ) {
5499
if (task.timed) {
5500
markOperationEndTime(request, task, performance.now());
5501
}
@@ -5605,7 +5633,10 @@ function finishAbortedTask(
5633
}
5634
forwardDebugInfoFromAbortedTask(request, task);
5635
// Track when we aborted this task as its end time.
5608
- if (enableProfilerTimer && enableComponentPerformanceTrack) {
5636
+ if (
5637
+ enableProfilerTimer &&
5638
+ (enableComponentPerformanceTrack || enableAsyncDebugInfo)
5639
+ ) {
5640
if (task.timed) {
5641
markOperationEndTime(request, task, request.abortTime);
5642
}
@@ -5921,7 +5952,10 @@ export function abort(request: Request, reason: mixed): void {
5952
}
5953
try {
5954
request.status = ABORTING;
5924
- if (enableProfilerTimer && enableComponentPerformanceTrack) {
5955
+ if (
5956
+ enableProfilerTimer &&
5957
+ (enableComponentPerformanceTrack || enableAsyncDebugInfo)
5958
+ ) {
5959
request.abortTime = performance.now();
5960
}
5961
request.cacheController.abort(reason);
packages/shared/ReactFeatureFlags.js
+1
-1
@@ -247,7 +247,7 @@ export const enableProfilerCommitHooks = __PROFILE__;
247
// Phase param passed to onRender callback differentiates between an "update" and a "cascading-update".
248
export const enableProfilerNestedUpdatePhase = __PROFILE__;
249
250
-export const enableAsyncDebugInfo = __EXPERIMENTAL__;
250
+export const enableAsyncDebugInfo = true;
251
252
// Track which Fiber(s) schedule render work.
253
export const enableUpdaterTracking = __PROFILE__;