Skip to content

Commit 2ba7b07

Browse files
authored
[DevTools] Compute a min and max range for the currently selected suspense boundary (#34201)
This computes a min and max range for the whole suspense boundary even when selecting a single component so that each component in a boundary has a consistent range. The start of this range is the earliest start of I/O in that boundary or the end of the previous suspense boundary, whatever is earlier. If the end of the previous boundary would make the range large, then we cap it since it's likely that the other boundary was just an independent render. The end of the range is the latest end of I/O in that boundary. If this is smaller than the end of the previous boundary plus the 300ms throttle, then we extend the end. This visualizes what throttling could potentially do if the previous boundary committed right at its end. Ofc, it might not have committed exactly at that time in this render. So this is just showing a potential throttle that could happen. To see actual throttle, you look in the Performance Track. <img width="661" height="353" alt="Screenshot 2025-08-14 at 12 41 43 AM" src="https://github.com/user-attachments/assets/b0155e5e-a83f-400c-a6b9-5c38a9d8a34f" /> We could come up with some annotation to highlight that this is eligible to be throttled in this case. If the lines don't extend to the edge, then it's likely it was throttled.
1 parent a96a0f3 commit 2ba7b07

File tree

6 files changed

+83
-1
lines changed

6 files changed

+83
-1
lines changed

packages/react-devtools-shared/src/backend/fiber/renderer.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5268,6 +5268,18 @@ export function attach(
52685268
}
52695269
}
52705270
5271+
function getNearestSuspenseNode(instance: DevToolsInstance): SuspenseNode {
5272+
while (instance.suspenseNode === null) {
5273+
if (instance.parent === null) {
5274+
throw new Error(
5275+
'There should always be a SuspenseNode parent on a mounted instance.',
5276+
);
5277+
}
5278+
instance = instance.parent;
5279+
}
5280+
return instance.suspenseNode;
5281+
}
5282+
52715283
function getNearestMountedDOMNode(publicInstance: Element): null | Element {
52725284
let domNode: null | Element = publicInstance;
52735285
while (domNode && !publicInstanceToDevToolsInstanceMap.has(domNode)) {
@@ -5556,6 +5568,56 @@ export function attach(
55565568
return result;
55575569
}
55585570
5571+
const FALLBACK_THROTTLE_MS: number = 300;
5572+
5573+
function getSuspendedByRange(
5574+
suspenseNode: SuspenseNode,
5575+
): null | [number, number] {
5576+
let min = Infinity;
5577+
let max = -Infinity;
5578+
suspenseNode.suspendedBy.forEach((_, ioInfo) => {
5579+
if (ioInfo.end > max) {
5580+
max = ioInfo.end;
5581+
}
5582+
if (ioInfo.start < min) {
5583+
min = ioInfo.start;
5584+
}
5585+
});
5586+
const parentSuspenseNode = suspenseNode.parent;
5587+
if (parentSuspenseNode !== null) {
5588+
let parentMax = -Infinity;
5589+
parentSuspenseNode.suspendedBy.forEach((_, ioInfo) => {
5590+
if (ioInfo.end > parentMax) {
5591+
parentMax = ioInfo.end;
5592+
}
5593+
});
5594+
// The parent max is theoretically the earlier the parent could've committed.
5595+
// Therefore, the theoretical max that the child could be throttled is that plus 300ms.
5596+
const throttleTime = parentMax + FALLBACK_THROTTLE_MS;
5597+
if (throttleTime > max) {
5598+
// If the theoretical throttle time is later than the earliest reveal then we extend
5599+
// the max time to show that this is timespan could possibly get throttled.
5600+
max = throttleTime;
5601+
}
5602+
5603+
// We use the end of the previous boundary as the start time for this boundary unless,
5604+
// that's earlier than we'd need to expand to the full fallback throttle range. It
5605+
// suggests that the parent was loaded earlier than this one.
5606+
let startTime = max - FALLBACK_THROTTLE_MS;
5607+
if (parentMax > startTime) {
5608+
startTime = parentMax;
5609+
}
5610+
// If the first fetch of this boundary starts before that, then we use that as the start.
5611+
if (startTime < min) {
5612+
min = startTime;
5613+
}
5614+
}
5615+
if (min < Infinity && max > -Infinity) {
5616+
return [min, max];
5617+
}
5618+
return null;
5619+
}
5620+
55595621
function getAwaitStackFromHooks(
55605622
hooks: HooksTree,
55615623
asyncInfo: ReactAsyncInfo,
@@ -6024,6 +6086,10 @@ export function attach(
60246086
: fiberInstance.suspendedBy.map(info =>
60256087
serializeAsyncInfo(info, fiberInstance, hooks),
60266088
);
6089+
const suspendedByRange = getSuspendedByRange(
6090+
getNearestSuspenseNode(fiberInstance),
6091+
);
6092+
60276093
return {
60286094
id: fiberInstance.id,
60296095
@@ -6086,6 +6152,7 @@ export function attach(
60866152
: Array.from(componentLogsEntry.warnings.entries()),
60876153
60886154
suspendedBy: suspendedBy,
6155+
suspendedByRange: suspendedByRange,
60896156
60906157
// List of owners
60916158
owners,
@@ -6144,6 +6211,9 @@ export function attach(
61446211
61456212
// Things that Suspended this Server Component (use(), awaits and direct child promises)
61466213
const suspendedBy = virtualInstance.suspendedBy;
6214+
const suspendedByRange = getSuspendedByRange(
6215+
getNearestSuspenseNode(virtualInstance),
6216+
);
61476217
61486218
return {
61496219
id: virtualInstance.id,
@@ -6196,6 +6266,7 @@ export function attach(
61966266
: suspendedBy.map(info =>
61976267
serializeAsyncInfo(info, virtualInstance, null),
61986268
),
6269+
suspendedByRange: suspendedByRange,
61996270
62006271
// List of owners
62016272
owners,

packages/react-devtools-shared/src/backend/legacy/renderer.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -858,6 +858,7 @@ export function attach(
858858

859859
// Not supported in legacy renderers.
860860
suspendedBy: [],
861+
suspendedByRange: null,
861862

862863
// List of owners
863864
owners,

packages/react-devtools-shared/src/backend/types.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ export type InspectedElement = {
300300

301301
// Things that suspended this Instances
302302
suspendedBy: Object, // DehydratedData or Array<SerializedAsyncInfo>
303+
suspendedByRange: null | [number, number],
303304

304305
// List of owners
305306
owners: Array<SerializedElement> | null,

packages/react-devtools-shared/src/backendAPI.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ export function convertInspectedElementBackendToFrontend(
270270
errors,
271271
warnings,
272272
suspendedBy,
273+
suspendedByRange,
273274
nativeTag,
274275
} = inspectedElementBackend;
275276

@@ -313,6 +314,7 @@ export function convertInspectedElementBackendToFrontend(
313314
hydratedSuspendedBy == null // backwards compat
314315
? []
315316
: hydratedSuspendedBy.map(backendToFrontendSerializedAsyncInfo),
317+
suspendedByRange,
316318
nativeTag,
317319
};
318320

packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ export default function InspectedElementSuspendedBy({
292292
inspectedElement,
293293
store,
294294
}: Props): React.Node {
295-
const {suspendedBy} = inspectedElement;
295+
const {suspendedBy, suspendedByRange} = inspectedElement;
296296

297297
// Skip the section if nothing suspended this component.
298298
if (suspendedBy == null || suspendedBy.length === 0) {
@@ -306,6 +306,11 @@ export default function InspectedElementSuspendedBy({
306306

307307
let minTime = Infinity;
308308
let maxTime = -Infinity;
309+
if (suspendedByRange !== null) {
310+
// The range of the whole suspense boundary.
311+
minTime = suspendedByRange[0];
312+
maxTime = suspendedByRange[1];
313+
}
309314
for (let i = 0; i < suspendedBy.length; i++) {
310315
const asyncInfo: SerializedAsyncInfo = suspendedBy[i];
311316
if (asyncInfo.awaited.start < minTime) {

packages/react-devtools-shared/src/frontend/types.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,8 @@ export type InspectedElement = {
279279

280280
// Things that suspended this Instances
281281
suspendedBy: Object,
282+
// Minimum start time to maximum end time + a potential (not actual) throttle, within the nearest boundary.
283+
suspendedByRange: null | [number, number],
282284

283285
// List of owners
284286
owners: Array<SerializedElement> | null,

0 commit comments

Comments
 (0)