mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
[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.
This commit is contained in:
parent
a96a0f3903
commit
2ba7b07ce1
|
|
@ -5268,6 +5268,18 @@ export function attach(
|
|||
}
|
||||
}
|
||||
|
||||
function getNearestSuspenseNode(instance: DevToolsInstance): SuspenseNode {
|
||||
while (instance.suspenseNode === null) {
|
||||
if (instance.parent === null) {
|
||||
throw new Error(
|
||||
'There should always be a SuspenseNode parent on a mounted instance.',
|
||||
);
|
||||
}
|
||||
instance = instance.parent;
|
||||
}
|
||||
return instance.suspenseNode;
|
||||
}
|
||||
|
||||
function getNearestMountedDOMNode(publicInstance: Element): null | Element {
|
||||
let domNode: null | Element = publicInstance;
|
||||
while (domNode && !publicInstanceToDevToolsInstanceMap.has(domNode)) {
|
||||
|
|
@ -5556,6 +5568,56 @@ export function attach(
|
|||
return result;
|
||||
}
|
||||
|
||||
const FALLBACK_THROTTLE_MS: number = 300;
|
||||
|
||||
function getSuspendedByRange(
|
||||
suspenseNode: SuspenseNode,
|
||||
): null | [number, number] {
|
||||
let min = Infinity;
|
||||
let max = -Infinity;
|
||||
suspenseNode.suspendedBy.forEach((_, ioInfo) => {
|
||||
if (ioInfo.end > max) {
|
||||
max = ioInfo.end;
|
||||
}
|
||||
if (ioInfo.start < min) {
|
||||
min = ioInfo.start;
|
||||
}
|
||||
});
|
||||
const parentSuspenseNode = suspenseNode.parent;
|
||||
if (parentSuspenseNode !== null) {
|
||||
let parentMax = -Infinity;
|
||||
parentSuspenseNode.suspendedBy.forEach((_, ioInfo) => {
|
||||
if (ioInfo.end > parentMax) {
|
||||
parentMax = ioInfo.end;
|
||||
}
|
||||
});
|
||||
// The parent max is theoretically the earlier the parent could've committed.
|
||||
// Therefore, the theoretical max that the child could be throttled is that plus 300ms.
|
||||
const throttleTime = parentMax + FALLBACK_THROTTLE_MS;
|
||||
if (throttleTime > max) {
|
||||
// If the theoretical throttle time is later than the earliest reveal then we extend
|
||||
// the max time to show that this is timespan could possibly get throttled.
|
||||
max = throttleTime;
|
||||
}
|
||||
|
||||
// We use the end of the previous boundary as the start time for this boundary unless,
|
||||
// that's earlier than we'd need to expand to the full fallback throttle range. It
|
||||
// suggests that the parent was loaded earlier than this one.
|
||||
let startTime = max - FALLBACK_THROTTLE_MS;
|
||||
if (parentMax > startTime) {
|
||||
startTime = parentMax;
|
||||
}
|
||||
// If the first fetch of this boundary starts before that, then we use that as the start.
|
||||
if (startTime < min) {
|
||||
min = startTime;
|
||||
}
|
||||
}
|
||||
if (min < Infinity && max > -Infinity) {
|
||||
return [min, max];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getAwaitStackFromHooks(
|
||||
hooks: HooksTree,
|
||||
asyncInfo: ReactAsyncInfo,
|
||||
|
|
@ -6024,6 +6086,10 @@ export function attach(
|
|||
: fiberInstance.suspendedBy.map(info =>
|
||||
serializeAsyncInfo(info, fiberInstance, hooks),
|
||||
);
|
||||
const suspendedByRange = getSuspendedByRange(
|
||||
getNearestSuspenseNode(fiberInstance),
|
||||
);
|
||||
|
||||
return {
|
||||
id: fiberInstance.id,
|
||||
|
||||
|
|
@ -6086,6 +6152,7 @@ export function attach(
|
|||
: Array.from(componentLogsEntry.warnings.entries()),
|
||||
|
||||
suspendedBy: suspendedBy,
|
||||
suspendedByRange: suspendedByRange,
|
||||
|
||||
// List of owners
|
||||
owners,
|
||||
|
|
@ -6144,6 +6211,9 @@ export function attach(
|
|||
|
||||
// Things that Suspended this Server Component (use(), awaits and direct child promises)
|
||||
const suspendedBy = virtualInstance.suspendedBy;
|
||||
const suspendedByRange = getSuspendedByRange(
|
||||
getNearestSuspenseNode(virtualInstance),
|
||||
);
|
||||
|
||||
return {
|
||||
id: virtualInstance.id,
|
||||
|
|
@ -6196,6 +6266,7 @@ export function attach(
|
|||
: suspendedBy.map(info =>
|
||||
serializeAsyncInfo(info, virtualInstance, null),
|
||||
),
|
||||
suspendedByRange: suspendedByRange,
|
||||
|
||||
// List of owners
|
||||
owners,
|
||||
|
|
|
|||
|
|
@ -858,6 +858,7 @@ export function attach(
|
|||
|
||||
// Not supported in legacy renderers.
|
||||
suspendedBy: [],
|
||||
suspendedByRange: null,
|
||||
|
||||
// List of owners
|
||||
owners,
|
||||
|
|
|
|||
|
|
@ -300,6 +300,7 @@ export type InspectedElement = {
|
|||
|
||||
// Things that suspended this Instances
|
||||
suspendedBy: Object, // DehydratedData or Array<SerializedAsyncInfo>
|
||||
suspendedByRange: null | [number, number],
|
||||
|
||||
// List of owners
|
||||
owners: Array<SerializedElement> | null,
|
||||
|
|
|
|||
|
|
@ -270,6 +270,7 @@ export function convertInspectedElementBackendToFrontend(
|
|||
errors,
|
||||
warnings,
|
||||
suspendedBy,
|
||||
suspendedByRange,
|
||||
nativeTag,
|
||||
} = inspectedElementBackend;
|
||||
|
||||
|
|
@ -313,6 +314,7 @@ export function convertInspectedElementBackendToFrontend(
|
|||
hydratedSuspendedBy == null // backwards compat
|
||||
? []
|
||||
: hydratedSuspendedBy.map(backendToFrontendSerializedAsyncInfo),
|
||||
suspendedByRange,
|
||||
nativeTag,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -292,7 +292,7 @@ export default function InspectedElementSuspendedBy({
|
|||
inspectedElement,
|
||||
store,
|
||||
}: Props): React.Node {
|
||||
const {suspendedBy} = inspectedElement;
|
||||
const {suspendedBy, suspendedByRange} = inspectedElement;
|
||||
|
||||
// Skip the section if nothing suspended this component.
|
||||
if (suspendedBy == null || suspendedBy.length === 0) {
|
||||
|
|
@ -306,6 +306,11 @@ export default function InspectedElementSuspendedBy({
|
|||
|
||||
let minTime = Infinity;
|
||||
let maxTime = -Infinity;
|
||||
if (suspendedByRange !== null) {
|
||||
// The range of the whole suspense boundary.
|
||||
minTime = suspendedByRange[0];
|
||||
maxTime = suspendedByRange[1];
|
||||
}
|
||||
for (let i = 0; i < suspendedBy.length; i++) {
|
||||
const asyncInfo: SerializedAsyncInfo = suspendedBy[i];
|
||||
if (asyncInfo.awaited.start < minTime) {
|
||||
|
|
|
|||
|
|
@ -279,6 +279,8 @@ export type InspectedElement = {
|
|||
|
||||
// Things that suspended this Instances
|
||||
suspendedBy: Object,
|
||||
// Minimum start time to maximum end time + a potential (not actual) throttle, within the nearest boundary.
|
||||
suspendedByRange: null | [number, number],
|
||||
|
||||
// List of owners
|
||||
owners: Array<SerializedElement> | null,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user