[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.
This commit is contained in:
Sebastian Markbåge 2025-08-17 16:17:11 -04:00 committed by GitHub
parent 7a36dfedc7
commit 42b1b33a24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 45 additions and 8 deletions

View File

@ -3343,6 +3343,7 @@ export function attach(
}
let start = -1;
let end = -1;
let byteSize = 0;
// $FlowFixMe[method-unbinding]
if (typeof performance.getEntriesByType === 'function') {
// We may be able to collect the start and end time of this resource from Performance Observer.
@ -3352,6 +3353,8 @@ export function attach(
if (resourceEntry.name === href) {
start = resourceEntry.startTime;
end = start + resourceEntry.duration;
// $FlowFixMe[prop-missing]
byteSize = (resourceEntry.transferSize: any) || 0;
}
}
}
@ -3367,6 +3370,10 @@ export function attach(
// $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file.
owner: fiber, // Allow linking to the <link> if it's not filtered.
};
if (byteSize > 0) {
// $FlowFixMe[cannot-write]
ioInfo.byteSize = byteSize;
}
const asyncInfo: ReactAsyncInfo = {
awaited: ioInfo,
// $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file.
@ -3431,6 +3438,7 @@ export function attach(
}
let start = -1;
let end = -1;
let byteSize = 0;
let fileSize = 0;
// $FlowFixMe[method-unbinding]
if (typeof performance.getEntriesByType === 'function') {
@ -3442,7 +3450,9 @@ export function attach(
start = resourceEntry.startTime;
end = start + resourceEntry.duration;
// $FlowFixMe[prop-missing]
fileSize = (resourceEntry.encodedBodySize: any) || 0;
fileSize = (resourceEntry.decodedBodySize: any) || 0;
// $FlowFixMe[prop-missing]
byteSize = (resourceEntry.transferSize: any) || 0;
}
}
}
@ -3476,6 +3486,10 @@ export function attach(
// $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file.
owner: fiber, // Allow linking to the <link> if it's not filtered.
};
if (byteSize > 0) {
// $FlowFixMe[cannot-write]
ioInfo.byteSize = byteSize;
}
const asyncInfo: ReactAsyncInfo = {
awaited: ioInfo,
// $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file.
@ -4704,16 +4718,15 @@ export function attach(
trackDebugInfoFromLazyType(nextFiber);
trackDebugInfoFromUsedThenables(nextFiber);
if (
nextFiber.tag === HostHoistable &&
prevFiber.memoizedState !== nextFiber.memoizedState
) {
if (nextFiber.tag === HostHoistable) {
const nearestInstance = reconcilingParent;
if (nearestInstance === null) {
throw new Error('Did not expect a host hoistable to be the root');
}
releaseHostResource(nearestInstance, prevFiber.memoizedState);
aquireHostResource(nearestInstance, nextFiber.memoizedState);
if (prevFiber.memoizedState !== nextFiber.memoizedState) {
releaseHostResource(nearestInstance, prevFiber.memoizedState);
aquireHostResource(nearestInstance, nextFiber.memoizedState);
}
trackDebugInfoFromHostResource(nearestInstance, nextFiber);
} else if (
nextFiber.tag === HostComponent ||
@ -5948,6 +5961,7 @@ export function attach(
description: getIODescription(resolvedValue),
start: ioInfo.start,
end: ioInfo.end,
byteSize: ioInfo.byteSize == null ? null : ioInfo.byteSize,
value: ioInfo.value == null ? null : ioInfo.value,
env: ioInfo.env == null ? null : ioInfo.env,
owner:

View File

@ -239,6 +239,7 @@ export type SerializedIOInfo = {
description: string,
start: number,
end: number,
byteSize: null | number,
value: null | Promise<mixed>,
env: null | string,
owner: null | SerializedElement,

View File

@ -221,6 +221,7 @@ function backendToFrontendSerializedAsyncInfo(
description: ioInfo.description,
start: ioInfo.start,
end: ioInfo.end,
byteSize: ioInfo.byteSize,
value: ioInfo.value,
env: ioInfo.env,
owner:

View File

@ -76,6 +76,19 @@ function getShortDescription(name: string, description: string): string {
return '';
}
function formatBytes(bytes: number) {
if (bytes < 1_000) {
return bytes + ' bytes';
}
if (bytes < 1_000_000) {
return (bytes / 1_000).toFixed(1) + ' kB';
}
if (bytes < 1_000_000_000) {
return (bytes / 1_000_000).toFixed(1) + ' mB';
}
return (bytes / 1_000_000_000).toFixed(1) + ' gB';
}
function SuspendedByRow({
bridge,
element,
@ -145,7 +158,13 @@ function SuspendedByRow({
<Button
className={styles.CollapsableHeader}
onClick={() => setIsOpen(prevIsOpen => !prevIsOpen)}
title={longName + ' — ' + (end - start).toFixed(2) + ' ms'}>
title={
longName +
' — ' +
(end - start).toFixed(2) +
' ms' +
(ioInfo.byteSize != null ? ' — ' + formatBytes(ioInfo.byteSize) : '')
}>
<ButtonIcon
className={styles.CollapsableHeaderIcon}
type={isOpen ? 'expanded' : 'collapsed'}

View File

@ -207,6 +207,7 @@ export type SerializedIOInfo = {
description: string,
start: number,
end: number,
byteSize: null | number,
value: null | Promise<mixed>,
env: null | string,
owner: null | SerializedElement,

View File

@ -237,6 +237,7 @@ export type ReactIOInfo = {
+name: string, // the name of the async function being called (e.g. "fetch")
+start: number, // the start time
+end: number, // the end time (this might be different from the time the await was unblocked)
+byteSize?: number, // the byte size of this resource across the network. (should only be included if affecting the client.)
+value?: null | Promise<mixed>, // the Promise that was awaited if any, may be rejected
+env?: string, // the environment where this I/O was spawned.
+owner?: null | ReactComponentInfo,