Skip to content

Commit 6773248

Browse files
authored
[DevTools] Track whether a boundary is currently suspended and make transparent (#34853)
This makes the rects that are currently in a suspended state appear ghostly so that you can see where along the timeline you are in the rects screen. <img width="451" height="407" alt="Screenshot 2025-10-14 at 11 43 20 PM" src="https://github.com/user-attachments/assets/f89e362b-a0d5-46e3-8171-564909715cd1" />
1 parent 5747cad commit 6773248

File tree

8 files changed

+76
-24
lines changed

8 files changed

+76
-24
lines changed

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

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2139,8 +2139,8 @@ export function attach(
21392139
// Regular operations
21402140
pendingOperations.length +
21412141
// All suspender changes are batched in a single message.
2142-
// [SUSPENSE_TREE_OPERATION_SUSPENDERS, suspenderChangesLength, ...[id, hasUniqueSuspenders]]
2143-
(numSuspenderChanges > 0 ? 2 + numSuspenderChanges * 2 : 0),
2142+
// [SUSPENSE_TREE_OPERATION_SUSPENDERS, suspenderChangesLength, ...[id, hasUniqueSuspenders, isSuspended]]
2143+
(numSuspenderChanges > 0 ? 2 + numSuspenderChanges * 3 : 0),
21442144
);
21452145
21462146
// Identify which renderer this update is coming from.
@@ -2225,6 +2225,14 @@ export function attach(
22252225
}
22262226
operations[i++] = fiberIdWithChanges;
22272227
operations[i++] = suspense.hasUniqueSuspenders ? 1 : 0;
2228+
const instance = suspense.instance;
2229+
const isSuspended =
2230+
// TODO: Track if other SuspenseNode like SuspenseList rows are suspended.
2231+
(instance.kind === FIBER_INSTANCE ||
2232+
instance.kind === FILTERED_FIBER_INSTANCE) &&
2233+
instance.data.tag === SuspenseComponent &&
2234+
instance.data.memoizedState !== null;
2235+
operations[i++] = isSuspended ? 1 : 0;
22282236
operations[i++] = suspense.environments.size;
22292237
suspense.environments.forEach((count, env) => {
22302238
operations[i++] = getStringID(env);
@@ -2657,9 +2665,15 @@ export function attach(
26572665
const fiber = fiberInstance.data;
26582666
const props = fiber.memoizedProps;
26592667
// TODO: Compute a fallback name based on Owner, key etc.
2660-
const name = props === null ? null : props.name || null;
2668+
const name =
2669+
fiber.tag !== SuspenseComponent || props === null
2670+
? null
2671+
: props.name || null;
26612672
const nameStringID = getStringID(name);
26622673
2674+
const isSuspended =
2675+
fiber.tag === SuspenseComponent && fiber.memoizedState !== null;
2676+
26632677
if (__DEBUG__) {
26642678
console.log('recordSuspenseMount()', suspenseInstance);
26652679
}
@@ -2670,6 +2684,7 @@ export function attach(
26702684
pushOperation(fiberID);
26712685
pushOperation(parentID);
26722686
pushOperation(nameStringID);
2687+
pushOperation(isSuspended ? 1 : 0);
26732688
26742689
const rects = suspenseInstance.rects;
26752690
if (rects === null) {
@@ -5038,15 +5053,24 @@ export function attach(
50385053
const nextIsSuspended = isSuspendedOffscreen(nextFiber);
50395054
50405055
if (isLegacySuspense) {
5041-
if (
5042-
fiberInstance !== null &&
5043-
fiberInstance.suspenseNode !== null &&
5044-
(prevFiber.stateNode === null) !== (nextFiber.stateNode === null)
5045-
) {
5046-
trackThrownPromisesFromRetryCache(
5047-
fiberInstance.suspenseNode,
5048-
nextFiber.stateNode,
5049-
);
5056+
if (fiberInstance !== null && fiberInstance.suspenseNode !== null) {
5057+
const suspenseNode = fiberInstance.suspenseNode;
5058+
if (
5059+
(prevFiber.stateNode === null) !==
5060+
(nextFiber.stateNode === null)
5061+
) {
5062+
trackThrownPromisesFromRetryCache(
5063+
suspenseNode,
5064+
nextFiber.stateNode,
5065+
);
5066+
}
5067+
if (
5068+
(prevFiber.memoizedState === null) !==
5069+
(nextFiber.memoizedState === null)
5070+
) {
5071+
// Toggle suspended state.
5072+
recordSuspenseSuspenders(suspenseNode);
5073+
}
50505074
}
50515075
}
50525076
// The logic below is inspired by the code paths in updateSuspenseComponent()
@@ -5194,6 +5218,14 @@ export function attach(
51945218
);
51955219
}
51965220
5221+
if (
5222+
(prevFiber.memoizedState === null) !==
5223+
(nextFiber.memoizedState === null)
5224+
) {
5225+
// Toggle suspended state.
5226+
recordSuspenseSuspenders(suspenseNode);
5227+
}
5228+
51975229
shouldMeasureSuspenseNode = false;
51985230
updateFlags |= updateSuspenseChildrenRecursively(
51995231
nextContentFiber,
@@ -5220,6 +5252,8 @@ export function attach(
52205252
}
52215253
52225254
trackThrownPromisesFromRetryCache(suspenseNode, nextFiber.stateNode);
5255+
// Toggle suspended state.
5256+
recordSuspenseSuspenders(suspenseNode);
52235257
52245258
mountSuspenseChildrenRecursively(
52255259
nextContentFiber,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ export function attach(
417417
pushOperation(id);
418418
pushOperation(parentID);
419419
pushOperation(getStringID(null)); // name
420+
pushOperation(0); // isSuspended
420421
// TODO: Measure rect of root
421422
pushOperation(-1);
422423
} else {

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,7 +1552,8 @@ export default class Store extends EventEmitter<{
15521552
const id = operations[i + 1];
15531553
const parentID = operations[i + 2];
15541554
const nameStringID = operations[i + 3];
1555-
const numRects = ((operations[i + 4]: any): number);
1555+
const isSuspended = operations[i + 4] === 1;
1556+
const numRects = ((operations[i + 5]: any): number);
15561557
let name = stringTable[nameStringID];
15571558

15581559
if (this._idToSuspense.has(id)) {
@@ -1579,7 +1580,7 @@ export default class Store extends EventEmitter<{
15791580
}
15801581
}
15811582

1582-
i += 5;
1583+
i += 6;
15831584
let rects: SuspenseNode['rects'];
15841585
if (numRects === -1) {
15851586
rects = null;
@@ -1625,6 +1626,7 @@ export default class Store extends EventEmitter<{
16251626
name,
16261627
rects,
16271628
hasUniqueSuspenders: false,
1629+
isSuspended: isSuspended,
16281630
});
16291631

16301632
hasSuspenseTreeChanged = true;
@@ -1801,6 +1803,7 @@ export default class Store extends EventEmitter<{
18011803
for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) {
18021804
const id = operations[i++];
18031805
const hasUniqueSuspenders = operations[i++] === 1;
1806+
const isSuspended = operations[i++] === 1;
18041807
const environmentNamesLength = operations[i++];
18051808
const environmentNames = [];
18061809
for (
@@ -1832,6 +1835,7 @@ export default class Store extends EventEmitter<{
18321835
}
18331836

18341837
suspense.hasUniqueSuspenders = hasUniqueSuspenders;
1838+
suspense.isSuspended = isSuspended;
18351839
// TODO: Recompute the environment names.
18361840
}
18371841

packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,8 @@ function updateTree(
378378
const fiberID = operations[i + 1];
379379
const parentID = operations[i + 2];
380380
const nameStringID = operations[i + 3];
381-
const numRects = operations[i + 4];
381+
const isSuspended = operations[i + 4];
382+
const numRects = operations[i + 5];
382383
const name = stringTable[nameStringID];
383384

384385
if (__DEBUG__) {
@@ -388,16 +389,16 @@ function updateTree(
388389
} else {
389390
rects =
390391
'[' +
391-
operations.slice(i + 5, i + 5 + numRects * 4).join(',') +
392+
operations.slice(i + 6, i + 6 + numRects * 4).join(',') +
392393
']';
393394
}
394395
debug(
395396
'Add suspense',
396-
`node ${fiberID} (name=${JSON.stringify(name)}, rects={${rects}}) under ${parentID}`,
397+
`node ${fiberID} (name=${JSON.stringify(name)}, rects={${rects}}) under ${parentID} suspended ${isSuspended}`,
397398
);
398399
}
399400

400-
i += 5 + (numRects === -1 ? 0 : numRects * 4);
401+
i += 6 + (numRects === -1 ? 0 : numRects * 4);
401402
break;
402403
}
403404

@@ -459,12 +460,13 @@ function updateTree(
459460
for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) {
460461
const suspenseNodeId = operations[i++];
461462
const hasUniqueSuspenders = operations[i++] === 1;
463+
const isSuspended = operations[i++] === 1;
462464
const environmentNamesLength = operations[i++];
463465
i += environmentNamesLength;
464466
if (__DEBUG__) {
465467
debug(
466468
'Suspender changes',
467-
`Suspense node ${suspenseNodeId} unique suspenders set to ${String(hasUniqueSuspenders)} with ${String(environmentNamesLength)} environments`,
469+
`Suspense node ${suspenseNodeId} unique suspenders set to ${String(hasUniqueSuspenders)} is suspended set to ${String(isSuspended)} with ${String(environmentNamesLength)} environments`,
468470
);
469471
}
470472
}

packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@
4444
outline-width: 0;
4545
}
4646

47+
.SuspenseRectsScaledRect[data-suspended='true'] {
48+
opacity: 0.3;
49+
}
50+
4751
/* highlight this boundary */
4852
.SuspenseRectsBoundary:hover:not(:has(.SuspenseRectsBoundary:hover)) > .SuspenseRectsRect, .SuspenseRectsBoundary[data-highlighted='true'] > .SuspenseRectsRect {
4953
background-color: var(--color-background-hover);

packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@ function ScaledRect({
3535
className,
3636
rect,
3737
visible,
38+
suspended,
3839
...props
3940
}: {
4041
className: string,
4142
rect: Rect,
4243
visible: boolean,
44+
suspended: boolean,
4345
...
4446
}): React$Node {
4547
const viewBox = useContext(ViewBox);
@@ -53,6 +55,7 @@ function ScaledRect({
5355
{...props}
5456
className={styles.SuspenseRectsScaledRect + ' ' + className}
5557
data-visible={visible}
58+
data-suspended={suspended}
5659
style={{
5760
width,
5861
height,
@@ -145,7 +148,8 @@ function SuspenseRects({
145148
<ScaledRect
146149
rect={boundingBox}
147150
className={styles.SuspenseRectsBoundary}
148-
visible={visible}>
151+
visible={visible}
152+
suspended={suspense.isSuspended}>
149153
<ViewBox.Provider value={boundingBox}>
150154
{visible &&
151155
suspense.rects !== null &&

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ export type SuspenseNode = {
200200
name: string | null,
201201
rects: null | Array<Rect>,
202202
hasUniqueSuspenders: boolean,
203+
isSuspended: boolean,
203204
};
204205

205206
// Serialized version of ReactIOInfo

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -340,9 +340,10 @@ export function printOperationsArray(operations: Array<number>) {
340340
const fiberID = operations[i + 1];
341341
const parentID = operations[i + 2];
342342
const nameStringID = operations[i + 3];
343-
const numRects = operations[i + 4];
343+
const isSuspended = operations[i + 4];
344+
const numRects = operations[i + 5];
344345

345-
i += 5;
346+
i += 6;
346347

347348
const name = stringTable[nameStringID];
348349
let rects: string;
@@ -368,7 +369,7 @@ export function printOperationsArray(operations: Array<number>) {
368369
}
369370

370371
logs.push(
371-
`Add suspense node ${fiberID} (${String(name)},rects={${rects}}) under ${parentID}`,
372+
`Add suspense node ${fiberID} (${String(name)},rects={${rects}}) under ${parentID} suspended ${isSuspended}`,
372373
);
373374
break;
374375
}
@@ -431,10 +432,11 @@ export function printOperationsArray(operations: Array<number>) {
431432
for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) {
432433
const id = operations[i++];
433434
const hasUniqueSuspenders = operations[i++] === 1;
435+
const isSuspended = operations[i++] === 1;
434436
const environmentNamesLength = operations[i++];
435437
i += environmentNamesLength;
436438
logs.push(
437-
`Suspense node ${id} unique suspenders set to ${String(hasUniqueSuspenders)} with ${String(environmentNamesLength)} environments`,
439+
`Suspense node ${id} unique suspenders set to ${String(hasUniqueSuspenders)} is suspended set to ${String(isSuspended)} with ${String(environmentNamesLength)} environments`,
438440
);
439441
}
440442

0 commit comments

Comments
 (0)