diff --git a/packages/react-reconciler/src/ReactDebugFiberPerf.js b/packages/react-reconciler/src/ReactDebugFiberPerf.js index 99be2fe897..818c821d2f 100644 --- a/packages/react-reconciler/src/ReactDebugFiberPerf.js +++ b/packages/react-reconciler/src/ReactDebugFiberPerf.js @@ -20,6 +20,7 @@ import { ContextProvider, ContextConsumer, Mode, + SuspenseComponent, } from 'shared/ReactWorkTags'; type MeasurementPhase = @@ -315,7 +316,10 @@ export function stopFailedWorkTimer(fiber: Fiber): void { return; } fiber._debugIsCurrentlyTiming = false; - const warning = 'An error was thrown inside this error boundary'; + const warning = + fiber.tag === SuspenseComponent + ? 'Rendering was suspended' + : 'An error was thrown inside this error boundary'; endFiberMark(fiber, null, warning); } } diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index c6bcf94324..7a7e91ef3f 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -56,7 +56,7 @@ import ReactStrictModeWarnings from './ReactStrictModeWarnings'; import warning from 'shared/warning'; import warningWithoutStack from 'shared/warningWithoutStack'; import * as ReactCurrentFiber from './ReactCurrentFiber'; -import {cancelWorkTimer} from './ReactDebugFiberPerf'; +import {startWorkTimer, cancelWorkTimer} from './ReactDebugFiberPerf'; import { mountChildFibers, @@ -720,11 +720,15 @@ function mountIndeterminateComponent( Component !== null && typeof Component.then === 'function' ) { + // We can't start a User Timing measurement with correct label yet. + // Cancel and resume right after we know the tag. + cancelWorkTimer(workInProgress); Component = readLazyComponentType(Component); const resolvedTag = (workInProgress.tag = resolveLazyComponentTag( workInProgress, Component, )); + startWorkTimer(workInProgress); const resolvedProps = resolveDefaultProps(Component, props); let child; switch (resolvedTag) { diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js index 801e64b148..a6bb1a7663 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js @@ -555,6 +555,58 @@ describe('ReactDebugFiberPerf', () => { expect(getFlameChart()).toMatchSnapshot(); }); + it('supports pure', () => { + const PureFoo = React.pure(function Foo() { + return
; + }); + ReactNoop.render( + + + , + ); + ReactNoop.flush(); + expect(getFlameChart()).toMatchSnapshot(); + }); + + it('supports Suspense and lazy', async () => { + function Spinner() { + return ; + } + + let resolve; + const LazyFoo = React.lazy( + () => + new Promise(r => { + resolve = r; + }), + ); + + ReactNoop.render( + + }> + + + , + ); + ReactNoop.flush(); + expect(getFlameChart()).toMatchSnapshot(); + + resolve(function Foo() { + return
; + }); + await LazyFoo; + + ReactNoop.render( + + + + + , + ); + ReactNoop.flush(); + expect(getFlameChart()).toMatchSnapshot(); + }); + it('does not schedule an extra callback if setState is called during a synchronous commit phase', () => { class Component extends React.Component { state = {step: 1}; diff --git a/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap b/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap index dcef20d968..24f4454f5e 100644 --- a/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap +++ b/packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap @@ -362,6 +362,40 @@ exports[`ReactDebugFiberPerf skips parents during setState 1`] = ` " `; +exports[`ReactDebugFiberPerf supports Suspense and lazy 1`] = ` +"⚛ (Waiting for async callback... will force flush in 5250 ms) + +⚛ (React Tree Reconciliation: Completed Root) + ⚛ Parent [mount] + ⛔ Suspense [mount] Warning: Rendering was suspended + ⚛ Suspense [mount] + ⚛ Spinner [mount] +" +`; + +exports[`ReactDebugFiberPerf supports Suspense and lazy 2`] = ` +"⚛ (Waiting for async callback... will force flush in 5250 ms) + +⚛ (React Tree Reconciliation: Completed Root) + ⚛ Parent [mount] + ⛔ Suspense [mount] Warning: Rendering was suspended + ⚛ Suspense [mount] + ⚛ Spinner [mount] + +⚛ (Waiting for async callback... will force flush in 5250 ms) + +⚛ (React Tree Reconciliation: Completed Root) + ⚛ Parent [mount] + ⚛ Suspense [mount] + ⚛ Foo [mount] + +⚛ (Committing Changes) + ⚛ (Committing Snapshot Effects: 0 Total) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) +" +`; + exports[`ReactDebugFiberPerf supports portals 1`] = ` "⚛ (Waiting for async callback... will force flush in 5250 ms) @@ -376,6 +410,20 @@ exports[`ReactDebugFiberPerf supports portals 1`] = ` " `; +exports[`ReactDebugFiberPerf supports pure 1`] = ` +"⚛ (Waiting for async callback... will force flush in 5250 ms) + +⚛ (React Tree Reconciliation: Completed Root) + ⚛ Parent [mount] + ⚛ Pure(Foo) [mount] + +⚛ (Committing Changes) + ⚛ (Committing Snapshot Effects: 0 Total) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) +" +`; + exports[`ReactDebugFiberPerf warns if an in-progress update is interrupted 1`] = ` "⚛ (Waiting for async callback... will force flush in 5250 ms) diff --git a/packages/shared/getComponentName.js b/packages/shared/getComponentName.js index 1b91ee04dd..4e89efef4a 100644 --- a/packages/shared/getComponentName.js +++ b/packages/shared/getComponentName.js @@ -16,6 +16,7 @@ import { REACT_FORWARD_REF_TYPE, REACT_FRAGMENT_TYPE, REACT_PORTAL_TYPE, + REACT_PURE_TYPE, REACT_PROFILER_TYPE, REACT_PROVIDER_TYPE, REACT_STRICT_MODE_TYPE, @@ -23,6 +24,18 @@ import { } from 'shared/ReactSymbols'; import {refineResolvedThenable} from 'shared/ReactLazyComponent'; +function getWrappedName( + outerType: mixed, + innerType: any, + wrapperName: string, +): string { + const functionName = innerType.displayName || innerType.name || ''; + return ( + (outerType: any).displayName || + (functionName !== '' ? `${wrapperName}(${functionName})` : wrapperName) + ); +} + function getComponentName(type: mixed): string | null { if (type == null) { // Host root, text node or just invalid type. @@ -64,12 +77,9 @@ function getComponentName(type: mixed): string | null { case REACT_PROVIDER_TYPE: return 'Context.Provider'; case REACT_FORWARD_REF_TYPE: - const renderFn = (type.render: any); - const functionName = renderFn.displayName || renderFn.name || ''; - return ( - (type: any).displayName || - (functionName !== '' ? `ForwardRef(${functionName})` : 'ForwardRef') - ); + return getWrappedName(type, type.render, 'ForwardRef'); + case REACT_PURE_TYPE: + return getWrappedName(type, type.render, 'Pure'); } if (typeof type.then === 'function') { const thenable: Thenable = (type: any);