From c6bb26bf833c5d91760daf28fa2750b81067ac30 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin <28902667+hoxyq@users.noreply.github.com> Date: Mon, 9 Feb 2026 22:18:17 +0000 Subject: [PATCH] [DevTools] Don't capture durations for disconnected subtrees when profiling (#35718) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After https://github.com/facebook/react/pull/34089, when updating (possibly, mounting) inside disconnected subtree, we don't record this as an operation. This only happens during reconnect. The issue is that `recordProfilingDurations()` can be called, which diffs tree base duration and reports it to the Frontend: https://github.com/facebook/react/blob/65db1000b944c8a07b5947c06b38eb8364dce4f2/packages/react-devtools-shared/src/backend/fiber/renderer.js#L4506-L4521 This operation can be recorded before the "Add" operation, and it will not be resolved properly on the Frontend side. Before the fix: ``` commit tree › Suspense › should handle transitioning from fallback back to content during profiling Could not clone the node: commit tree does not contain fiber "5". This is a bug in React DevTools. 162 | const existingNode = nodes.get(id); 163 | if (existingNode == null) { > 164 | throw new Error( | ^ 165 | `Could not clone the node: commit tree does not contain fiber "${id}". This is a bug in React DevTools.`, 166 | ); 167 | } at getClonedNode (packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js:164:13) at updateTree (packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js:348:24) at getCommitTree (packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js:112:20) at ProfilingCache.getCommitTree (packages/react-devtools-shared/src/devtools/ProfilingCache.js:40:46) at Object. (packages/react-devtools-shared/src/__tests__/profilingCommitTreeBuilder-test.js:257:44) ``` --- .../profilingCommitTreeBuilder-test.js | 69 +++++++++++++++++++ .../src/backend/fiber/renderer.js | 10 +-- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/profilingCommitTreeBuilder-test.js b/packages/react-devtools-shared/src/__tests__/profilingCommitTreeBuilder-test.js index f08629dd05..5879032bad 100644 --- a/packages/react-devtools-shared/src/__tests__/profilingCommitTreeBuilder-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilingCommitTreeBuilder-test.js @@ -191,4 +191,73 @@ describe('commit tree', () => { expect(commitTrees[1].nodes.size).toBe(2); // + }); }); + + describe('Suspense', () => { + it('should handle transitioning from fallback back to content during profiling', async () => { + let resolvePromise; + let promise = null; + let childTreeBaseDuration = 10; + + function Wrapper({children}) { + Scheduler.unstable_advanceTime(2); + return children; + } + + function Child() { + Scheduler.unstable_advanceTime(childTreeBaseDuration); + if (promise !== null) { + React.use(promise); + } + return ; + } + + function GrandChild() { + Scheduler.unstable_advanceTime(5); + return null; + } + + function Fallback() { + Scheduler.unstable_advanceTime(2); + return null; + } + + function App() { + Scheduler.unstable_advanceTime(1); + return ( + }> + + + + + ); + } + + utils.act(() => store.profilerStore.startProfiling()); + + // Commit 1: Mount with primary + utils.act(() => render()); + + // Commit 2: Suspend, show fallback + promise = new Promise(resolve => (resolvePromise = resolve)); + await utils.actAsync(() => render()); + + // Commit 3: Resolve suspended promise, show primary content with a different duration. + childTreeBaseDuration = 20; + promise = null; + await utils.actAsync(resolvePromise); + utils.act(() => render()); + + utils.act(() => store.profilerStore.stopProfiling()); + + const rootID = store.roots[0]; + const dataForRoot = store.profilerStore.getDataForRoot(rootID); + const numCommits = dataForRoot.commitData.length; + for (let commitIndex = 0; commitIndex < numCommits; commitIndex++) { + store.profilerStore.profilingCache.getCommitTree({ + commitIndex, + rootID, + }); + } + }); + }); }); diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 5a977dc762..3214c83922 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -5581,10 +5581,12 @@ export function attach( } recordConsoleLogs(fiberInstance, componentLogsEntry); - const isProfilingSupported = - nextFiber.hasOwnProperty('treeBaseDuration'); - if (isProfilingSupported) { - recordProfilingDurations(fiberInstance, prevFiber); + if (!isInDisconnectedSubtree) { + const isProfilingSupported = + nextFiber.hasOwnProperty('treeBaseDuration'); + if (isProfilingSupported) { + recordProfilingDurations(fiberInstance, prevFiber); + } } } }