From ae0dc789ec9233c0046d9e483e7b7c7ef909f9d6 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Wed, 17 Sep 2025 15:36:32 +0200 Subject: [PATCH 1/2] [DevTools] Ignore renderers without support for a dedicated Suspense tree Mostly so that we don't trigger unnecessary warnings in older React versions. --- .../src/backend/fiber/renderer.js | 2 ++ .../src/backend/legacy/renderer.js | 2 ++ .../src/devtools/store.js | 21 +++++++++++++++++-- .../src/devtools/utils.js | 2 +- .../views/Profiler/CommitTreeBuilder.js | 1 + .../views/SuspenseTab/SuspenseRects.js | 4 +++- .../views/SuspenseTab/SuspenseTimeline.js | 5 ++++- .../views/SuspenseTab/SuspenseTreeContext.js | 4 +++- packages/react-devtools-shared/src/utils.js | 1 + 9 files changed, 36 insertions(+), 6 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 33786a41877b8..98deb66baf475 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -1065,6 +1065,7 @@ export function attach( scheduleUpdate, getCurrentFiber, } = renderer; + const supportsSuspenseTree = true; const supportsTogglingError = typeof setErrorHandler === 'function' && typeof scheduleUpdate === 'function'; @@ -2394,6 +2395,7 @@ export function attach( ); pushOperation(hasOwnerMetadata ? 1 : 0); pushOperation(supportsTogglingSuspense ? 1 : 0); + pushOperation(supportsSuspenseTree ? 1 : 0); if (isProfiling) { if (displayNamesByRootID !== null) { diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index 2915d2cd30554..81a88cf730cfa 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -181,6 +181,7 @@ export function attach( } const supportsTogglingSuspense = false; + const supportsSuspenseTree = false; function getDisplayNameForElementID(id: number): string | null { const internalInstance = idToInternalInstanceMap.get(id); @@ -411,6 +412,7 @@ export function attach( pushOperation(0); // StrictMode supported? pushOperation(hasOwnerMetadata ? 1 : 0); pushOperation(supportsTogglingSuspense ? 1 : 0); + pushOperation(supportsSuspenseTree ? 1 : 0); } else { const type = getElementType(internalInstance); const {displayName, key} = getData(internalInstance); diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index bb540f09daf30..239dd3ddd2728 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -90,6 +90,7 @@ export type Capabilities = { supportsBasicProfiling: boolean, hasOwnerMetadata: boolean, supportsStrictMode: boolean, + supportsSuspenseTree: boolean, supportsTogglingSuspense: boolean, supportsTimeline: boolean, }; @@ -493,6 +494,14 @@ export default class Store extends EventEmitter<{ ); } + supportsSuspenseTree(rootID: Element['id']): boolean { + const capabilities = this._rootIDToCapabilities.get(rootID); + if (capabilities === undefined) { + throw new Error(`No capabilities registered for root ${rootID}`); + } + return capabilities.supportsSuspenseTree; + } + supportsTogglingSuspense(rootID: Element['id']): boolean { const capabilities = this._rootIDToCapabilities.get(rootID); if (capabilities === undefined) { @@ -895,11 +904,14 @@ export default class Store extends EventEmitter<{ if (root === null) { return []; } - if (!this.supportsTogglingSuspense(root.id)) { + if ( + !this.supportsTogglingSuspense(rootID) || + !this.supportsSuspenseTree(rootID) + ) { return []; } const list: SuspenseNode['id'][] = []; - const suspense = this.getSuspenseByID(root.id); + const suspense = this.getSuspenseByID(rootID); if (suspense !== null) { const stack = [suspense]; while (stack.length > 0) { @@ -1170,6 +1182,7 @@ export default class Store extends EventEmitter<{ let supportsStrictMode = false; let hasOwnerMetadata = false; let supportsTogglingSuspense = false; + let supportsSuspenseTree = false; // If we don't know the bridge protocol, guess that we're dealing with the latest. // If we do know it, we can take it into consideration when parsing operations. @@ -1185,6 +1198,9 @@ export default class Store extends EventEmitter<{ supportsTogglingSuspense = operations[i] > 0; i++; + + supportsSuspenseTree = operations[i] > 0; + i++; } this._roots = this._roots.concat(id); @@ -1193,6 +1209,7 @@ export default class Store extends EventEmitter<{ supportsBasicProfiling, hasOwnerMetadata, supportsStrictMode, + supportsSuspenseTree, supportsTogglingSuspense, supportsTimeline, }); diff --git a/packages/react-devtools-shared/src/devtools/utils.js b/packages/react-devtools-shared/src/devtools/utils.js index d5078679f44f9..e421ab9ecc28e 100644 --- a/packages/react-devtools-shared/src/devtools/utils.js +++ b/packages/react-devtools-shared/src/devtools/utils.js @@ -176,7 +176,7 @@ export function printStore( rootWeight += weight; - if (includeSuspense) { + if (includeSuspense && store.supportsSuspenseTree(rootID)) { const root = store.getSuspenseByID(rootID); // Roots from legacy renderers don't have a separate Suspense tree if (root !== null) { diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js index 5637967a6abb2..cc412cfadc1a5 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js @@ -210,6 +210,7 @@ function updateTree( i++; // supportsStrictMode flag i++; // hasOwnerMetadata flag i++; // supportsTogglingSuspense flag + i++; // supportsSuspenseTree flag if (__DEBUG__) { debug('Add', `new root fiber ${id}`); diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js index 39c6f1c492517..6cbf1c9e97d4e 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js @@ -190,7 +190,9 @@ function getDocumentBoundingRect( for (let i = 0; i < roots.length; i++) { const rootID = roots[i]; - const root = store.getSuspenseByID(rootID); + const root = store.supportsSuspenseTree(rootID) + ? store.getSuspenseByID(rootID) + : null; if (root === null) { continue; } diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js index dd58703cb97c8..3b6da1d21d44c 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js @@ -243,7 +243,10 @@ export default function SuspenseTimeline(): React$Node { const name = '#' + rootID; // TODO: Highlight host on hover return ( - ); diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTreeContext.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTreeContext.js index 3f0c5fd41ae2b..b4e0f7bfa6d8a 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTreeContext.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTreeContext.js @@ -82,7 +82,9 @@ type Props = { function getDefaultRootID(store: Store): Element['id'] | null { const designatedRootID = store.roots.find(rootID => { - const suspense = store.getSuspenseByID(rootID); + const suspense = store.supportsSuspenseTree(rootID) + ? store.getSuspenseByID(rootID) + : null; return ( store.supportsTogglingSuspense(rootID) && suspense !== null && diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index 7e256febea013..47debe8a95f05 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -263,6 +263,7 @@ export function printOperationsArray(operations: Array) { i++; // supportsStrictMode i++; // hasOwnerMetadata i++; // supportsTogglingSuspense + i++; // supportsSuspenseTree } else { const parentID = ((operations[i]: any): number); i++; From d94b3cfd1c44012ddf5a5e217ea16b08486f5d3d Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Wed, 17 Sep 2025 20:58:08 +0200 Subject: [PATCH 2/2] [DevTools] Record Suspense node for roots in legacy renderers --- .../src/backend/fiber/renderer.js | 2 -- .../src/backend/legacy/renderer.js | 24 ++++++++++++++++--- .../src/devtools/store.js | 19 +-------------- .../src/devtools/utils.js | 2 +- .../views/Profiler/CommitTreeBuilder.js | 1 - .../views/SuspenseTab/SuspenseRects.js | 4 +--- .../views/SuspenseTab/SuspenseTimeline.js | 5 +--- .../views/SuspenseTab/SuspenseTreeContext.js | 4 +--- packages/react-devtools-shared/src/utils.js | 1 - 9 files changed, 26 insertions(+), 36 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 98deb66baf475..33786a41877b8 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -1065,7 +1065,6 @@ export function attach( scheduleUpdate, getCurrentFiber, } = renderer; - const supportsSuspenseTree = true; const supportsTogglingError = typeof setErrorHandler === 'function' && typeof scheduleUpdate === 'function'; @@ -2395,7 +2394,6 @@ export function attach( ); pushOperation(hasOwnerMetadata ? 1 : 0); pushOperation(supportsTogglingSuspense ? 1 : 0); - pushOperation(supportsSuspenseTree ? 1 : 0); if (isProfiling) { if (displayNamesByRootID !== null) { diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index 81a88cf730cfa..b59c0292942c6 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -34,6 +34,8 @@ import { TREE_OPERATION_ADD, TREE_OPERATION_REMOVE, TREE_OPERATION_REORDER_CHILDREN, + SUSPENSE_TREE_OPERATION_ADD, + SUSPENSE_TREE_OPERATION_REMOVE, UNKNOWN_SUSPENDERS_NONE, } from '../../constants'; import {decorateMany, forceUpdate, restoreMany} from './utils'; @@ -181,7 +183,6 @@ export function attach( } const supportsTogglingSuspense = false; - const supportsSuspenseTree = false; function getDisplayNameForElementID(id: number): string | null { const internalInstance = idToInternalInstanceMap.get(id); @@ -412,7 +413,13 @@ export function attach( pushOperation(0); // StrictMode supported? pushOperation(hasOwnerMetadata ? 1 : 0); pushOperation(supportsTogglingSuspense ? 1 : 0); - pushOperation(supportsSuspenseTree ? 1 : 0); + + pushOperation(SUSPENSE_TREE_OPERATION_ADD); + pushOperation(id); + pushOperation(parentID); + pushOperation(getStringID(null)); // name + // TODO: Measure rect of root + pushOperation(-1); } else { const type = getElementType(internalInstance); const {displayName, key} = getData(internalInstance); @@ -451,7 +458,12 @@ export function attach( } function recordUnmount(internalInstance: InternalInstance, id: number) { - pendingUnmountedIDs.push(id); + const isRoot = parentIDStack.length === 0; + if (isRoot) { + pendingUnmountedRootID = id; + } else { + pendingUnmountedIDs.push(id); + } idToInternalInstanceMap.delete(id); } @@ -521,6 +533,8 @@ export function attach( // All unmounts are batched in a single message. // [TREE_OPERATION_REMOVE, removedIDLength, ...ids] (numUnmountIDs > 0 ? 2 + numUnmountIDs : 0) + + // [SUSPENSE_TREE_OPERATION_REMOVE, 1, pendingUnmountedRootID] + (pendingUnmountedRootID === null ? 0 : 3) + // Mount operations pendingOperations.length, ); @@ -557,6 +571,10 @@ export function attach( if (pendingUnmountedRootID !== null) { operations[i] = pendingUnmountedRootID; i++; + + operations[i++] = SUSPENSE_TREE_OPERATION_REMOVE; + operations[i++] = 1; + operations[i++] = pendingUnmountedRootID; } } diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index 239dd3ddd2728..79e6957aecfe4 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -90,7 +90,6 @@ export type Capabilities = { supportsBasicProfiling: boolean, hasOwnerMetadata: boolean, supportsStrictMode: boolean, - supportsSuspenseTree: boolean, supportsTogglingSuspense: boolean, supportsTimeline: boolean, }; @@ -494,14 +493,6 @@ export default class Store extends EventEmitter<{ ); } - supportsSuspenseTree(rootID: Element['id']): boolean { - const capabilities = this._rootIDToCapabilities.get(rootID); - if (capabilities === undefined) { - throw new Error(`No capabilities registered for root ${rootID}`); - } - return capabilities.supportsSuspenseTree; - } - supportsTogglingSuspense(rootID: Element['id']): boolean { const capabilities = this._rootIDToCapabilities.get(rootID); if (capabilities === undefined) { @@ -904,10 +895,7 @@ export default class Store extends EventEmitter<{ if (root === null) { return []; } - if ( - !this.supportsTogglingSuspense(rootID) || - !this.supportsSuspenseTree(rootID) - ) { + if (!this.supportsTogglingSuspense(rootID)) { return []; } const list: SuspenseNode['id'][] = []; @@ -1182,7 +1170,6 @@ export default class Store extends EventEmitter<{ let supportsStrictMode = false; let hasOwnerMetadata = false; let supportsTogglingSuspense = false; - let supportsSuspenseTree = false; // If we don't know the bridge protocol, guess that we're dealing with the latest. // If we do know it, we can take it into consideration when parsing operations. @@ -1198,9 +1185,6 @@ export default class Store extends EventEmitter<{ supportsTogglingSuspense = operations[i] > 0; i++; - - supportsSuspenseTree = operations[i] > 0; - i++; } this._roots = this._roots.concat(id); @@ -1209,7 +1193,6 @@ export default class Store extends EventEmitter<{ supportsBasicProfiling, hasOwnerMetadata, supportsStrictMode, - supportsSuspenseTree, supportsTogglingSuspense, supportsTimeline, }); diff --git a/packages/react-devtools-shared/src/devtools/utils.js b/packages/react-devtools-shared/src/devtools/utils.js index e421ab9ecc28e..d5078679f44f9 100644 --- a/packages/react-devtools-shared/src/devtools/utils.js +++ b/packages/react-devtools-shared/src/devtools/utils.js @@ -176,7 +176,7 @@ export function printStore( rootWeight += weight; - if (includeSuspense && store.supportsSuspenseTree(rootID)) { + if (includeSuspense) { const root = store.getSuspenseByID(rootID); // Roots from legacy renderers don't have a separate Suspense tree if (root !== null) { diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js index cc412cfadc1a5..5637967a6abb2 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js @@ -210,7 +210,6 @@ function updateTree( i++; // supportsStrictMode flag i++; // hasOwnerMetadata flag i++; // supportsTogglingSuspense flag - i++; // supportsSuspenseTree flag if (__DEBUG__) { debug('Add', `new root fiber ${id}`); diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js index 6cbf1c9e97d4e..39c6f1c492517 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js @@ -190,9 +190,7 @@ function getDocumentBoundingRect( for (let i = 0; i < roots.length; i++) { const rootID = roots[i]; - const root = store.supportsSuspenseTree(rootID) - ? store.getSuspenseByID(rootID) - : null; + const root = store.getSuspenseByID(rootID); if (root === null) { continue; } diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js index 3b6da1d21d44c..dd58703cb97c8 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js @@ -243,10 +243,7 @@ export default function SuspenseTimeline(): React$Node { const name = '#' + rootID; // TODO: Highlight host on hover return ( - ); diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTreeContext.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTreeContext.js index b4e0f7bfa6d8a..3f0c5fd41ae2b 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTreeContext.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTreeContext.js @@ -82,9 +82,7 @@ type Props = { function getDefaultRootID(store: Store): Element['id'] | null { const designatedRootID = store.roots.find(rootID => { - const suspense = store.supportsSuspenseTree(rootID) - ? store.getSuspenseByID(rootID) - : null; + const suspense = store.getSuspenseByID(rootID); return ( store.supportsTogglingSuspense(rootID) && suspense !== null && diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index 47debe8a95f05..7e256febea013 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -263,7 +263,6 @@ export function printOperationsArray(operations: Array) { i++; // supportsStrictMode i++; // hasOwnerMetadata i++; // supportsTogglingSuspense - i++; // supportsSuspenseTree } else { const parentID = ((operations[i]: any): number); i++;