From f85f8704319bee16c04b610823b2f62a0da1edfb Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 8 Aug 2025 14:48:11 -0400 Subject: [PATCH 1/3] Add additional debugInfo to React.lazy constructors in DEV --- packages/react/src/ReactLazy.js | 85 ++++++++++++++++++++++++++++++++- packages/shared/ReactTypes.js | 1 + 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/packages/react/src/ReactLazy.js b/packages/react/src/ReactLazy.js index 2ac29c87774ec..c49e0e2906101 100644 --- a/packages/react/src/ReactLazy.js +++ b/packages/react/src/ReactLazy.js @@ -7,7 +7,14 @@ * @flow */ -import type {Wakeable, Thenable, ReactDebugInfo} from 'shared/ReactTypes'; +import type { + Wakeable, + Thenable, + FulfilledThenable, + RejectedThenable, + ReactDebugInfo, + ReactIOInfo, +} from 'shared/ReactTypes'; import {REACT_LAZY_TYPE} from 'shared/ReactSymbols'; @@ -19,21 +26,25 @@ const Rejected = 2; type UninitializedPayload = { _status: -1, _result: () => Thenable<{default: T, ...}>, + _ioInfo?: ReactIOInfo, // DEV-only }; type PendingPayload = { _status: 0, _result: Wakeable, + _ioInfo?: ReactIOInfo, // DEV-only }; type ResolvedPayload = { _status: 1, _result: {default: T, ...}, + _ioInfo?: ReactIOInfo, // DEV-only }; type RejectedPayload = { _status: 2, _result: mixed, + _ioInfo?: ReactIOInfo, // DEV-only }; type Payload = @@ -51,6 +62,14 @@ export type LazyComponent = { function lazyInitializer(payload: Payload): T { if (payload._status === Uninitialized) { + if (__DEV__) { + const ioInfo = payload._ioInfo; + if (ioInfo != null) { + // Mark when we first kicked off the lazy request. + // $FlowFixMe[cannot-write] + ioInfo.start = ioInfo.end = performance.now(); + } + } const ctor = payload._result; const thenable = ctor(); // Transition to the next state. @@ -68,6 +87,21 @@ function lazyInitializer(payload: Payload): T { const resolved: ResolvedPayload = (payload: any); resolved._status = Resolved; resolved._result = moduleObject; + if (__DEV__) { + const ioInfo = payload._ioInfo; + if (ioInfo != null) { + // Mark the end time of when we resolved. + // $FlowFixMe[cannot-write] + ioInfo.end = performance.now(); + } + // Make the thenable introspectable + if (thenable.status === undefined) { + const fulfilledThenable: FulfilledThenable<{default: T, ...}> = + (thenable: any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = moduleObject; + } + } } }, error => { @@ -79,9 +113,37 @@ function lazyInitializer(payload: Payload): T { const rejected: RejectedPayload = (payload: any); rejected._status = Rejected; rejected._result = error; + if (__DEV__) { + const ioInfo = payload._ioInfo; + if (ioInfo != null) { + // Mark the end time of when we rejected. + // $FlowFixMe[cannot-write] + ioInfo.end = performance.now(); + } + // Make the thenable introspectable + if (thenable.status === undefined) { + const rejectedThenable: RejectedThenable<{default: T, ...}> = + (thenable: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = error; + } + } } }, ); + if (__DEV__) { + const ioInfo = payload._ioInfo; + if (ioInfo != null) { + // Stash the thenable for introspection of the value later. + // $FlowFixMe[cannot-write] + ioInfo.value = thenable; + const displayName = thenable.displayName; + if (typeof displayName === 'string') { + // $FlowFixMe[cannot-write] + ioInfo.name = displayName; + } + } + } if (payload._status === Uninitialized) { // In case, we're still uninitialized, then we're waiting for the thenable // to resolve. Set it as pending in the meantime. @@ -140,5 +202,26 @@ export function lazy( _init: lazyInitializer, }; + if (__DEV__) { + // TODO: We should really track the owner here but currently ReactIOInfo + // can only contain ReactComponentInfo and not a Fiber. It's unusual to + // create a lazy inside an owner though since they should be in module scope. + const owner = null; + const ioInfo: ReactIOInfo = { + name: 'lazy', + start: -1, + end: -1, + value: null, + owner: owner, + debugStack: new Error('react-stack-top-frame'), + // eslint-disable-next-line react-internal/no-production-logging + debugTask: console.createTask ? console.createTask('lazy()') : null, + }; + payload._ioInfo = ioInfo; + // Add debug info to the lazy, but this doesn't have an await stack yet. + // That will be inferred by later usage. + lazyType._debugInfo = [{awaited: ioInfo}]; + } + return lazyType; } diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 5c7af1d1b305f..943b7ca09a9eb 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -108,6 +108,7 @@ interface ThenableImpl { onFulfill: (value: T) => mixed, onReject: (error: mixed) => mixed, ): void | Wakeable; + displayName?: string; } interface UntrackedThenable extends ThenableImpl { status?: void; From f90cb1d495b4c6baaa20824e5a0f9faddd2f9733 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 8 Aug 2025 15:03:14 -0400 Subject: [PATCH 2/3] Update test We read a timestamp for start/end now. --- .../src/__tests__/ReactFlight-test.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 9a60c3bd66b29..47ce539820836 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -2822,7 +2822,7 @@ describe('ReactFlight', () => { expect(getDebugInfo(promise)).toEqual( __DEV__ ? [ - {time: 20}, + {time: 22}, { name: 'ServerComponent', env: 'Server', @@ -2832,7 +2832,7 @@ describe('ReactFlight', () => { transport: expect.arrayContaining([]), }, }, - {time: 21}, + {time: 23}, ] : undefined, ); @@ -2843,7 +2843,7 @@ describe('ReactFlight', () => { expect(getDebugInfo(thirdPartyChildren[0])).toEqual( __DEV__ ? [ - {time: 22}, // Clamped to the start + {time: 24}, // Clamped to the start { name: 'ThirdPartyComponent', env: 'third-party', @@ -2851,15 +2851,15 @@ describe('ReactFlight', () => { stack: ' in Object. (at **)', props: {}, }, - {time: 22}, - {time: 23}, // This last one is when the promise resolved into the first party. + {time: 24}, + {time: 25}, // This last one is when the promise resolved into the first party. ] : undefined, ); expect(getDebugInfo(thirdPartyChildren[1])).toEqual( __DEV__ ? [ - {time: 22}, // Clamped to the start + {time: 24}, // Clamped to the start { name: 'ThirdPartyLazyComponent', env: 'third-party', @@ -2867,14 +2867,14 @@ describe('ReactFlight', () => { stack: ' in myLazy (at **)\n in lazyInitializer (at **)', props: {}, }, - {time: 22}, + {time: 24}, ] : undefined, ); expect(getDebugInfo(thirdPartyChildren[2])).toEqual( __DEV__ ? [ - {time: 22}, + {time: 24}, { name: 'ThirdPartyFragmentComponent', env: 'third-party', @@ -2882,7 +2882,7 @@ describe('ReactFlight', () => { stack: ' in Object. (at **)', props: {}, }, - {time: 22}, + {time: 24}, ] : undefined, ); From 9876196dea82bc8930dce699bea172162b9abb03 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 8 Aug 2025 18:12:51 -0400 Subject: [PATCH 3/3] Gate on enableAsyncDebugInfo --- .../src/__tests__/ReactFlight-test.js | 18 +++++++++--------- packages/react/src/ReactLazy.js | 10 ++++++---- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 47ce539820836..0fd9b869c6141 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -2822,7 +2822,7 @@ describe('ReactFlight', () => { expect(getDebugInfo(promise)).toEqual( __DEV__ ? [ - {time: 22}, + {time: gate(flags => flags.enableAsyncDebugInfo) ? 22 : 20}, { name: 'ServerComponent', env: 'Server', @@ -2832,7 +2832,7 @@ describe('ReactFlight', () => { transport: expect.arrayContaining([]), }, }, - {time: 23}, + {time: gate(flags => flags.enableAsyncDebugInfo) ? 23 : 21}, ] : undefined, ); @@ -2843,7 +2843,7 @@ describe('ReactFlight', () => { expect(getDebugInfo(thirdPartyChildren[0])).toEqual( __DEV__ ? [ - {time: 24}, // Clamped to the start + {time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, // Clamped to the start { name: 'ThirdPartyComponent', env: 'third-party', @@ -2851,15 +2851,15 @@ describe('ReactFlight', () => { stack: ' in Object. (at **)', props: {}, }, - {time: 24}, - {time: 25}, // This last one is when the promise resolved into the first party. + {time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, + {time: gate(flags => flags.enableAsyncDebugInfo) ? 25 : 23}, // This last one is when the promise resolved into the first party. ] : undefined, ); expect(getDebugInfo(thirdPartyChildren[1])).toEqual( __DEV__ ? [ - {time: 24}, // Clamped to the start + {time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, // Clamped to the start { name: 'ThirdPartyLazyComponent', env: 'third-party', @@ -2867,14 +2867,14 @@ describe('ReactFlight', () => { stack: ' in myLazy (at **)\n in lazyInitializer (at **)', props: {}, }, - {time: 24}, + {time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, ] : undefined, ); expect(getDebugInfo(thirdPartyChildren[2])).toEqual( __DEV__ ? [ - {time: 24}, + {time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, { name: 'ThirdPartyFragmentComponent', env: 'third-party', @@ -2882,7 +2882,7 @@ describe('ReactFlight', () => { stack: ' in Object. (at **)', props: {}, }, - {time: 24}, + {time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, ] : undefined, ); diff --git a/packages/react/src/ReactLazy.js b/packages/react/src/ReactLazy.js index c49e0e2906101..69b35b58cc8bd 100644 --- a/packages/react/src/ReactLazy.js +++ b/packages/react/src/ReactLazy.js @@ -16,6 +16,8 @@ import type { ReactIOInfo, } from 'shared/ReactTypes'; +import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags'; + import {REACT_LAZY_TYPE} from 'shared/ReactSymbols'; const Uninitialized = -1; @@ -62,7 +64,7 @@ export type LazyComponent = { function lazyInitializer(payload: Payload): T { if (payload._status === Uninitialized) { - if (__DEV__) { + if (__DEV__ && enableAsyncDebugInfo) { const ioInfo = payload._ioInfo; if (ioInfo != null) { // Mark when we first kicked off the lazy request. @@ -113,7 +115,7 @@ function lazyInitializer(payload: Payload): T { const rejected: RejectedPayload = (payload: any); rejected._status = Rejected; rejected._result = error; - if (__DEV__) { + if (__DEV__ && enableAsyncDebugInfo) { const ioInfo = payload._ioInfo; if (ioInfo != null) { // Mark the end time of when we rejected. @@ -131,7 +133,7 @@ function lazyInitializer(payload: Payload): T { } }, ); - if (__DEV__) { + if (__DEV__ && enableAsyncDebugInfo) { const ioInfo = payload._ioInfo; if (ioInfo != null) { // Stash the thenable for introspection of the value later. @@ -202,7 +204,7 @@ export function lazy( _init: lazyInitializer, }; - if (__DEV__) { + if (__DEV__ && enableAsyncDebugInfo) { // TODO: We should really track the owner here but currently ReactIOInfo // can only contain ReactComponentInfo and not a Fiber. It's unusual to // create a lazy inside an owner though since they should be in module scope.