Skip to content

Commit 6445b31

Browse files
authored
[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.
1 parent ab5238d commit 6445b31

File tree

3 files changed

+96
-10
lines changed

3 files changed

+96
-10
lines changed

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2822,7 +2822,7 @@ describe('ReactFlight', () => {
28222822
expect(getDebugInfo(promise)).toEqual(
28232823
__DEV__
28242824
? [
2825-
{time: 20},
2825+
{time: gate(flags => flags.enableAsyncDebugInfo) ? 22 : 20},
28262826
{
28272827
name: 'ServerComponent',
28282828
env: 'Server',
@@ -2832,7 +2832,7 @@ describe('ReactFlight', () => {
28322832
transport: expect.arrayContaining([]),
28332833
},
28342834
},
2835-
{time: 21},
2835+
{time: gate(flags => flags.enableAsyncDebugInfo) ? 23 : 21},
28362836
]
28372837
: undefined,
28382838
);
@@ -2843,46 +2843,46 @@ describe('ReactFlight', () => {
28432843
expect(getDebugInfo(thirdPartyChildren[0])).toEqual(
28442844
__DEV__
28452845
? [
2846-
{time: 22}, // Clamped to the start
2846+
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, // Clamped to the start
28472847
{
28482848
name: 'ThirdPartyComponent',
28492849
env: 'third-party',
28502850
key: null,
28512851
stack: ' in Object.<anonymous> (at **)',
28522852
props: {},
28532853
},
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.
28562856
]
28572857
: undefined,
28582858
);
28592859
expect(getDebugInfo(thirdPartyChildren[1])).toEqual(
28602860
__DEV__
28612861
? [
2862-
{time: 22}, // Clamped to the start
2862+
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, // Clamped to the start
28632863
{
28642864
name: 'ThirdPartyLazyComponent',
28652865
env: 'third-party',
28662866
key: null,
28672867
stack: ' in myLazy (at **)\n in lazyInitializer (at **)',
28682868
props: {},
28692869
},
2870-
{time: 22},
2870+
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
28712871
]
28722872
: undefined,
28732873
);
28742874
expect(getDebugInfo(thirdPartyChildren[2])).toEqual(
28752875
__DEV__
28762876
? [
2877-
{time: 22},
2877+
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
28782878
{
28792879
name: 'ThirdPartyFragmentComponent',
28802880
env: 'third-party',
28812881
key: '3',
28822882
stack: ' in Object.<anonymous> (at **)',
28832883
props: {},
28842884
},
2885-
{time: 22},
2885+
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
28862886
]
28872887
: undefined,
28882888
);

packages/react/src/ReactLazy.js

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,16 @@
77
* @flow
88
*/
99

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';
1120

1221
import {REACT_LAZY_TYPE} from 'shared/ReactSymbols';
1322

@@ -19,21 +28,25 @@ const Rejected = 2;
1928
type UninitializedPayload<T> = {
2029
_status: -1,
2130
_result: () => Thenable<{default: T, ...}>,
31+
_ioInfo?: ReactIOInfo, // DEV-only
2232
};
2333

2434
type PendingPayload = {
2535
_status: 0,
2636
_result: Wakeable,
37+
_ioInfo?: ReactIOInfo, // DEV-only
2738
};
2839

2940
type ResolvedPayload<T> = {
3041
_status: 1,
3142
_result: {default: T, ...},
43+
_ioInfo?: ReactIOInfo, // DEV-only
3244
};
3345

3446
type RejectedPayload = {
3547
_status: 2,
3648
_result: mixed,
49+
_ioInfo?: ReactIOInfo, // DEV-only
3750
};
3851

3952
type Payload<T> =
@@ -51,6 +64,14 @@ export type LazyComponent<T, P> = {
5164

5265
function lazyInitializer<T>(payload: Payload<T>): T {
5366
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+
}
5475
const ctor = payload._result;
5576
const thenable = ctor();
5677
// Transition to the next state.
@@ -68,6 +89,21 @@ function lazyInitializer<T>(payload: Payload<T>): T {
6889
const resolved: ResolvedPayload<T> = (payload: any);
6990
resolved._status = Resolved;
7091
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+
}
71107
}
72108
},
73109
error => {
@@ -79,9 +115,37 @@ function lazyInitializer<T>(payload: Payload<T>): T {
79115
const rejected: RejectedPayload = (payload: any);
80116
rejected._status = Rejected;
81117
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+
}
82133
}
83134
},
84135
);
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+
}
85149
if (payload._status === Uninitialized) {
86150
// In case, we're still uninitialized, then we're waiting for the thenable
87151
// to resolve. Set it as pending in the meantime.
@@ -140,5 +204,26 @@ export function lazy<T>(
140204
_init: lazyInitializer,
141205
};
142206

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+
143228
return lazyType;
144229
}

packages/shared/ReactTypes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ interface ThenableImpl<T> {
108108
onFulfill: (value: T) => mixed,
109109
onReject: (error: mixed) => mixed,
110110
): void | Wakeable;
111+
displayName?: string;
111112
}
112113
interface UntrackedThenable<T> extends ThenableImpl<T> {
113114
status?: void;

0 commit comments

Comments
 (0)