[DevTools] Add byteSize field to ReactIOInfo and show this in the tooltip (#34221)
This is intended to be used by various client side resources where the transfer size is interesting to know how it'll perform in various network conditions. Not intended to be added by the server. For now it's only added internally by DevTools itself on img/css but I'll add it from Flight Client too in a follow up. This now shows this as the "transfer size" which is the encoded body size + headers/overhead. Where as the "fileSize" that I add to images is the decoded body size, like what you'd see on disk. This is what Chrome shows so it's less confusing if you compare Network tab and this view.
Sebastian Markbåge committed
Aug 17, 2025 at 16:17 UTC
42b1b33a244f91d00650d82105871d37cc120e64
6 files changed
+45
-8
packages/react-devtools-shared/src/backend/fiber/renderer.js
+21
-7
@@ -3343,6 +3343,7 @@ export function attach(
3343
}
3344
let start = -1;
3345
let end = -1;
3346
+ let byteSize = 0;
3347
// $FlowFixMe[method-unbinding]
3348
if (typeof performance.getEntriesByType === 'function') {
3349
// We may be able to collect the start and end time of this resource from Performance Observer.
@@ -3352,6 +3353,8 @@ export function attach(
3353
if (resourceEntry.name === href) {
3354
start = resourceEntry.startTime;
3355
end = start + resourceEntry.duration;
3356
+ // $FlowFixMe[prop-missing]
3357
+ byteSize = (resourceEntry.transferSize: any) || 0;
3358
}
3359
}
3360
}
@@ -3367,6 +3370,10 @@ export function attach(
3370
// $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file.
3371
owner: fiber, // Allow linking to the <link> if it's not filtered.
3372
};
3373
+ if (byteSize > 0) {
3374
+ // $FlowFixMe[cannot-write]
3375
+ ioInfo.byteSize = byteSize;
3376
+ }
3377
const asyncInfo: ReactAsyncInfo = {
3378
awaited: ioInfo,
3379
// $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file.
@@ -3431,6 +3438,7 @@ export function attach(
3438
}
3439
let start = -1;
3440
let end = -1;
3441
+ let byteSize = 0;
3442
let fileSize = 0;
3443
// $FlowFixMe[method-unbinding]
3444
if (typeof performance.getEntriesByType === 'function') {
@@ -3442,7 +3450,9 @@ export function attach(
3450
start = resourceEntry.startTime;
3451
end = start + resourceEntry.duration;
3452
// $FlowFixMe[prop-missing]
3445
- fileSize = (resourceEntry.encodedBodySize: any) || 0;
3453
+ fileSize = (resourceEntry.decodedBodySize: any) || 0;
3454
+ // $FlowFixMe[prop-missing]
3455
+ byteSize = (resourceEntry.transferSize: any) || 0;
3456
}
3457
}
3458
}
@@ -3476,6 +3486,10 @@ export function attach(
3486
// $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file.
3487
owner: fiber, // Allow linking to the <link> if it's not filtered.
3488
};
3489
+ if (byteSize > 0) {
3490
+ // $FlowFixMe[cannot-write]
3491
+ ioInfo.byteSize = byteSize;
3492
+ }
3493
const asyncInfo: ReactAsyncInfo = {
3494
awaited: ioInfo,
3495
// $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file.
@@ -4704,16 +4718,15 @@ export function attach(
4718
trackDebugInfoFromLazyType(nextFiber);
4719
trackDebugInfoFromUsedThenables(nextFiber);
4720
4707
- if (
4708
- nextFiber.tag === HostHoistable &&
4709
- prevFiber.memoizedState !== nextFiber.memoizedState
4710
- ) {
4721
+ if (nextFiber.tag === HostHoistable) {
4722
const nearestInstance = reconcilingParent;
4723
if (nearestInstance === null) {
4724
throw new Error('Did not expect a host hoistable to be the root');
4725
}
4715
- releaseHostResource(nearestInstance, prevFiber.memoizedState);
4716
- aquireHostResource(nearestInstance, nextFiber.memoizedState);
4726
+ if (prevFiber.memoizedState !== nextFiber.memoizedState) {
4727
+ releaseHostResource(nearestInstance, prevFiber.memoizedState);
4728
+ aquireHostResource(nearestInstance, nextFiber.memoizedState);
4729
+ }
4730
trackDebugInfoFromHostResource(nearestInstance, nextFiber);
4731
} else if (
4732
nextFiber.tag === HostComponent ||
@@ -5948,6 +5961,7 @@ export function attach(
5961
description: getIODescription(resolvedValue),
5962
start: ioInfo.start,
5963
end: ioInfo.end,
5964
+ byteSize: ioInfo.byteSize == null ? null : ioInfo.byteSize,
5965
value: ioInfo.value == null ? null : ioInfo.value,
5966
env: ioInfo.env == null ? null : ioInfo.env,
5967
owner:
packages/react-devtools-shared/src/backend/types.js
+1
@@ -239,6 +239,7 @@ export type SerializedIOInfo = {
239
description: string,
240
start: number,
241
end: number,
242
+ byteSize: null | number,
243
value: null | Promise<mixed>,
244
env: null | string,
245
owner: null | SerializedElement,
packages/react-devtools-shared/src/backendAPI.js
+1
@@ -221,6 +221,7 @@ function backendToFrontendSerializedAsyncInfo(
221
description: ioInfo.description,
222
start: ioInfo.start,
223
end: ioInfo.end,
224
+ byteSize: ioInfo.byteSize,
225
value: ioInfo.value,
226
env: ioInfo.env,
227
owner:
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js
+20
-1
@@ -76,6 +76,19 @@ function getShortDescription(name: string, description: string): string {
76
return '';
77
}
78
79
+function formatBytes(bytes: number) {
80
+ if (bytes < 1_000) {
81
+ return bytes + ' bytes';
82
+ }
83
+ if (bytes < 1_000_000) {
84
+ return (bytes / 1_000).toFixed(1) + ' kB';
85
+ }
86
+ if (bytes < 1_000_000_000) {
87
+ return (bytes / 1_000_000).toFixed(1) + ' mB';
88
+ }
89
+ return (bytes / 1_000_000_000).toFixed(1) + ' gB';
90
+}
91
+
92
function SuspendedByRow({
93
bridge,
94
element,
@@ -145,7 +158,13 @@ function SuspendedByRow({
158
<Button
159
className={styles.CollapsableHeader}
160
onClick={() => setIsOpen(prevIsOpen => !prevIsOpen)}
148
- title={longName + ' — ' + (end - start).toFixed(2) + ' ms'}>
161
+ title={
162
+ longName +
163
+ ' — ' +
164
+ (end - start).toFixed(2) +
165
+ ' ms' +
166
+ (ioInfo.byteSize != null ? ' — ' + formatBytes(ioInfo.byteSize) : '')
167
+ }>
168
<ButtonIcon
169
className={styles.CollapsableHeaderIcon}
170
type={isOpen ? 'expanded' : 'collapsed'}
packages/react-devtools-shared/src/frontend/types.js
+1
@@ -207,6 +207,7 @@ export type SerializedIOInfo = {
207
description: string,
208
start: number,
209
end: number,
210
+ byteSize: null | number,
211
value: null | Promise<mixed>,
212
env: null | string,
213
owner: null | SerializedElement,
packages/shared/ReactTypes.js
+1
@@ -237,6 +237,7 @@ export type ReactIOInfo = {
237
+name: string, // the name of the async function being called (e.g. "fetch")
238
+start: number, // the start time
239
+end: number, // the end time (this might be different from the time the await was unblocked)
240
+ +byteSize?: number, // the byte size of this resource across the network. (should only be included if affecting the client.)
241
+value?: null | Promise<mixed>, // the Promise that was awaited if any, may be rejected
242
+env?: string, // the environment where this I/O was spawned.
243
+owner?: null | ReactComponentInfo,