@samitouri / QOS-React / commits / 42b1b33a24

[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,