@samitouri / QOS-React / commits / 6445b3154e

[Fiber] Add additional debugInfo to React.lazy constructors in DEV (#34137)

This creates a debug info object for the React.lazy call when it's called on the client. We have some additional information we can track for these since they're created by React earlier. We can track the stack trace where `React.lazy` was called to associate it back to something useful. We can track the start time when we initialized it for the first time and the end time when it resolves. The name from the promise if available. This data is currently only picked up in child position and not component position. The component position is in a follow up. <img width="592" height="451" alt="Screenshot 2025-08-08 at 2 49 33 PM" src="https://github.com/user-attachments/assets/913d2629-6df5-40f6-b036-ae13631379b9" /> This begs for ignore listing in the front end since these stacks aren't filtered on the server.

Sebastian Markbåge committed Aug 11, 2025 at 11:42 UTC 6445b3154ee60c2b2aa15d8be437a3f07feeb8f9
3 files changed +96 -10
packages/react-client/src/__tests__/ReactFlight-test.js
+9 -9
@@ -2822,7 +2822,7 @@ describe('ReactFlight', () => {
2822 expect(getDebugInfo(promise)).toEqual(
2823 __DEV__
2824 ? [
2825 - {time: 20},
2825 + {time: gate(flags => flags.enableAsyncDebugInfo) ? 22 : 20},
2826 {
2827 name: 'ServerComponent',
2828 env: 'Server',
@@ -2832,7 +2832,7 @@ describe('ReactFlight', () => {
2832 transport: expect.arrayContaining([]),
2833 },
2834 },
2835 - {time: 21},
2835 + {time: gate(flags => flags.enableAsyncDebugInfo) ? 23 : 21},
2836 ]
2837 : undefined,
2838 );
@@ -2843,7 +2843,7 @@ describe('ReactFlight', () => {
2843 expect(getDebugInfo(thirdPartyChildren[0])).toEqual(
2844 __DEV__
2845 ? [
2846 - {time: 22}, // Clamped to the start
2846 + {time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, // Clamped to the start
2847 {
2848 name: 'ThirdPartyComponent',
2849 env: 'third-party',
@@ -2851,15 +2851,15 @@ describe('ReactFlight', () => {
2851 stack: ' in Object.<anonymous> (at **)',
2852 props: {},
2853 },
2854 - {time: 22},
2855 - {time: 23}, // This last one is when the promise resolved into the first party.
2854 + {time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
2855 + {time: gate(flags => flags.enableAsyncDebugInfo) ? 25 : 23}, // This last one is when the promise resolved into the first party.
2856 ]
2857 : undefined,
2858 );
2859 expect(getDebugInfo(thirdPartyChildren[1])).toEqual(
2860 __DEV__
2861 ? [
2862 - {time: 22}, // Clamped to the start
2862 + {time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, // Clamped to the start
2863 {
2864 name: 'ThirdPartyLazyComponent',
2865 env: 'third-party',
@@ -2867,14 +2867,14 @@ describe('ReactFlight', () => {
2867 stack: ' in myLazy (at **)\n in lazyInitializer (at **)',
2868 props: {},
2869 },
2870 - {time: 22},
2870 + {time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
2871 ]
2872 : undefined,
2873 );
2874 expect(getDebugInfo(thirdPartyChildren[2])).toEqual(
2875 __DEV__
2876 ? [
2877 - {time: 22},
2877 + {time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
2878 {
2879 name: 'ThirdPartyFragmentComponent',
2880 env: 'third-party',
@@ -2882,7 +2882,7 @@ describe('ReactFlight', () => {
2882 stack: ' in Object.<anonymous> (at **)',
2883 props: {},
2884 },
2885 - {time: 22},
2885 + {time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
2886 ]
2887 : undefined,
2888 );
packages/react/src/ReactLazy.js
+86 -1
@@ -7,7 +7,16 @@
7 * @flow
8 */
9
10 -import type {Wakeable, Thenable, ReactDebugInfo} from 'shared/ReactTypes';
10 +import type {
11 + Wakeable,
12 + Thenable,
13 + FulfilledThenable,
14 + RejectedThenable,
15 + ReactDebugInfo,
16 + ReactIOInfo,
17 +} from 'shared/ReactTypes';
18 +
19 +import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
20
21 import {REACT_LAZY_TYPE} from 'shared/ReactSymbols';
22
@@ -19,21 +28,25 @@ const Rejected = 2;
28 type UninitializedPayload<T> = {
29 _status: -1,
30 _result: () => Thenable<{default: T, ...}>,
31 + _ioInfo?: ReactIOInfo, // DEV-only
32 };
33
34 type PendingPayload = {
35 _status: 0,
36 _result: Wakeable,
37 + _ioInfo?: ReactIOInfo, // DEV-only
38 };
39
40 type ResolvedPayload<T> = {
41 _status: 1,
42 _result: {default: T, ...},
43 + _ioInfo?: ReactIOInfo, // DEV-only
44 };
45
46 type RejectedPayload = {
47 _status: 2,
48 _result: mixed,
49 + _ioInfo?: ReactIOInfo, // DEV-only
50 };
51
52 type Payload<T> =
@@ -51,6 +64,14 @@ export type LazyComponent<T, P> = {
64
65 function lazyInitializer<T>(payload: Payload<T>): T {
66 if (payload._status === Uninitialized) {
67 + if (__DEV__ && enableAsyncDebugInfo) {
68 + const ioInfo = payload._ioInfo;
69 + if (ioInfo != null) {
70 + // Mark when we first kicked off the lazy request.
71 + // $FlowFixMe[cannot-write]
72 + ioInfo.start = ioInfo.end = performance.now();
73 + }
74 + }
75 const ctor = payload._result;
76 const thenable = ctor();
77 // Transition to the next state.
@@ -68,6 +89,21 @@ function lazyInitializer<T>(payload: Payload<T>): T {
89 const resolved: ResolvedPayload<T> = (payload: any);
90 resolved._status = Resolved;
91 resolved._result = moduleObject;
92 + if (__DEV__) {
93 + const ioInfo = payload._ioInfo;
94 + if (ioInfo != null) {
95 + // Mark the end time of when we resolved.
96 + // $FlowFixMe[cannot-write]
97 + ioInfo.end = performance.now();
98 + }
99 + // Make the thenable introspectable
100 + if (thenable.status === undefined) {
101 + const fulfilledThenable: FulfilledThenable<{default: T, ...}> =
102 + (thenable: any);
103 + fulfilledThenable.status = 'fulfilled';
104 + fulfilledThenable.value = moduleObject;
105 + }
106 + }
107 }
108 },
109 error => {
@@ -79,9 +115,37 @@ function lazyInitializer<T>(payload: Payload<T>): T {
115 const rejected: RejectedPayload = (payload: any);
116 rejected._status = Rejected;
117 rejected._result = error;
118 + if (__DEV__ && enableAsyncDebugInfo) {
119 + const ioInfo = payload._ioInfo;
120 + if (ioInfo != null) {
121 + // Mark the end time of when we rejected.
122 + // $FlowFixMe[cannot-write]
123 + ioInfo.end = performance.now();
124 + }
125 + // Make the thenable introspectable
126 + if (thenable.status === undefined) {
127 + const rejectedThenable: RejectedThenable<{default: T, ...}> =
128 + (thenable: any);
129 + rejectedThenable.status = 'rejected';
130 + rejectedThenable.reason = error;
131 + }
132 + }
133 }
134 },
135 );
136 + if (__DEV__ && enableAsyncDebugInfo) {
137 + const ioInfo = payload._ioInfo;
138 + if (ioInfo != null) {
139 + // Stash the thenable for introspection of the value later.
140 + // $FlowFixMe[cannot-write]
141 + ioInfo.value = thenable;
142 + const displayName = thenable.displayName;
143 + if (typeof displayName === 'string') {
144 + // $FlowFixMe[cannot-write]
145 + ioInfo.name = displayName;
146 + }
147 + }
148 + }
149 if (payload._status === Uninitialized) {
150 // In case, we're still uninitialized, then we're waiting for the thenable
151 // to resolve. Set it as pending in the meantime.
@@ -140,5 +204,26 @@ export function lazy<T>(
204 _init: lazyInitializer,
205 };
206
207 + if (__DEV__ && enableAsyncDebugInfo) {
208 + // TODO: We should really track the owner here but currently ReactIOInfo
209 + // can only contain ReactComponentInfo and not a Fiber. It's unusual to
210 + // create a lazy inside an owner though since they should be in module scope.
211 + const owner = null;
212 + const ioInfo: ReactIOInfo = {
213 + name: 'lazy',
214 + start: -1,
215 + end: -1,
216 + value: null,
217 + owner: owner,
218 + debugStack: new Error('react-stack-top-frame'),
219 + // eslint-disable-next-line react-internal/no-production-logging
220 + debugTask: console.createTask ? console.createTask('lazy()') : null,
221 + };
222 + payload._ioInfo = ioInfo;
223 + // Add debug info to the lazy, but this doesn't have an await stack yet.
224 + // That will be inferred by later usage.
225 + lazyType._debugInfo = [{awaited: ioInfo}];
226 + }
227 +
228 return lazyType;
229 }
packages/shared/ReactTypes.js
+1
@@ -108,6 +108,7 @@ interface ThenableImpl<T> {
108 onFulfill: (value: T) => mixed,
109 onReject: (error: mixed) => mixed,
110 ): void | Wakeable;
111 + displayName?: string;
112 }
113 interface UntrackedThenable<T> extends ThenableImpl<T> {
114 status?: void;