Skip to content

Commit fa212fc

Browse files
authored
[DevTools] Measure the Rectangle of Suspense boundaries as we reconcile (facebook#34090)
Stacked on facebook#34089. This measures the client rects of the direct children of Suspense boundaries as we reconcile. This will be used by the Suspense tab to visualize the boundaries given their outlines. We could ask for this more lazily just in case we're currently looking at the Suspense tab. We could also do something like monitor the sizes using a ResizeObserver to cover when they change. However, it should be pretty cheap to this in the reconciliation phase since we're already mostly visiting these nodes on the way down. We have also already done all the layouts at this point since it was part of the commit phase and paint already. So we're just reading cached values in this phase. We can also infer that things are expected to change when parents or sibling changes. Similar technique as ViewTransitions.
1 parent b080063 commit fa212fc

File tree

1 file changed

+188
-0
lines changed
  • packages/react-devtools-shared/src/backend/fiber

1 file changed

+188
-0
lines changed

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

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,13 +271,17 @@ function createVirtualInstance(
271271

272272
type DevToolsInstance = FiberInstance | VirtualInstance | FilteredFiberInstance;
273273

274+
// A Generic Rect super type which can include DOMRect and other objects with similar shape like in React Native.
275+
type Rect = {x: number, y: number, width: number, height: number, ...};
276+
274277
type SuspenseNode = {
275278
// The Instance can be a Suspense boundary, a SuspenseList Row, or HostRoot.
276279
// It can also be disconnected from the main tree if it's a Filtered Instance.
277280
instance: FiberInstance | FilteredFiberInstance,
278281
parent: null | SuspenseNode,
279282
firstChild: null | SuspenseNode,
280283
nextSibling: null | SuspenseNode,
284+
rects: null | Array<Rect>, // The bounding rects of content children.
281285
suspendedBy: Map<ReactIOInfo, Set<DevToolsInstance>>, // Tracks which data we're suspended by and the children that suspend it.
282286
// Track whether any of the items in suspendedBy are unique this this Suspense boundaries or if they're all
283287
// also in the parent sets. This determine whether this could contribute in the loading sequence.
@@ -292,6 +296,7 @@ function createSuspenseNode(
292296
parent: null,
293297
firstChild: null,
294298
nextSibling: null,
299+
rects: null,
295300
suspendedBy: new Map(),
296301
hasUniqueSuspenders: false,
297302
});
@@ -2130,6 +2135,69 @@ export function attach(
21302135
pendingStringTableLength = 0;
21312136
}
21322137
2138+
function measureHostInstance(instance: HostInstance): null | Array<Rect> {
2139+
// Feature detect measurement capabilities of this environment.
2140+
// TODO: Consider making this capability injected by the ReactRenderer.
2141+
if (typeof instance !== 'object' || instance === null) {
2142+
return null;
2143+
}
2144+
if (typeof instance.getClientRects === 'function') {
2145+
// DOM
2146+
const result = [];
2147+
const doc = instance.ownerDocument;
2148+
const win = doc && doc.defaultView;
2149+
const scrollX = win ? win.scrollX : 0;
2150+
const scrollY = win ? win.scrollY : 0;
2151+
const rects = instance.getClientRects();
2152+
for (let i = 0; i < rects.length; i++) {
2153+
const rect = rects[i];
2154+
result.push({
2155+
x: rect.x + scrollX,
2156+
y: rect.y + scrollY,
2157+
width: rect.width,
2158+
height: rect.height,
2159+
});
2160+
}
2161+
return result;
2162+
}
2163+
if (instance.canonical) {
2164+
// Native
2165+
const publicInstance = instance.canonical.publicInstance;
2166+
if (!publicInstance) {
2167+
// The publicInstance may not have been initialized yet if there was no ref on this node.
2168+
// We can't initialize it from any existing Hook but we could fallback to this async form:
2169+
// renderer.extraDevToolsConfig.getInspectorDataForInstance(instance).hierarchy[last].getInspectorData().measure(callback)
2170+
return null;
2171+
}
2172+
if (typeof publicInstance.getBoundingClientRect === 'function') {
2173+
// enableAccessToHostTreeInFabric / ReadOnlyElement
2174+
return [publicInstance.getBoundingClientRect()];
2175+
}
2176+
if (typeof publicInstance.unstable_getBoundingClientRect === 'function') {
2177+
// ReactFabricHostComponent
2178+
return [publicInstance.unstable_getBoundingClientRect()];
2179+
}
2180+
}
2181+
return null;
2182+
}
2183+
2184+
function measureInstance(instance: DevToolsInstance): null | Array<Rect> {
2185+
// Synchronously return the client rects of the Host instances directly inside this Instance.
2186+
const hostInstances = findAllCurrentHostInstances(instance);
2187+
let result: null | Array<Rect> = null;
2188+
for (let i = 0; i < hostInstances.length; i++) {
2189+
const childResult = measureHostInstance(hostInstances[i]);
2190+
if (childResult !== null) {
2191+
if (result === null) {
2192+
result = childResult;
2193+
} else {
2194+
result = result.concat(childResult);
2195+
}
2196+
}
2197+
}
2198+
return result;
2199+
}
2200+
21332201
function getStringID(string: string | null): number {
21342202
if (string === null) {
21352203
return 0;
@@ -2439,6 +2507,10 @@ export function attach(
24392507
}
24402508
}
24412509
2510+
function recordSuspenseResize(suspenseNode: SuspenseNode): void {
2511+
// TODO: Notify the front end of the change.
2512+
}
2513+
24422514
// Running state of the remaining children from the previous version of this parent that
24432515
// we haven't yet added back. This should be reset anytime we change parent.
24442516
// Any remaining ones at the end will be deleted.
@@ -2768,6 +2840,79 @@ export function attach(
27682840
return false;
27692841
}
27702842
2843+
function areEqualRects(
2844+
a: null | Array<Rect>,
2845+
b: null | Array<Rect>,
2846+
): boolean {
2847+
if (a === null) {
2848+
return b === null;
2849+
}
2850+
if (b === null) {
2851+
return false;
2852+
}
2853+
if (a.length !== b.length) {
2854+
return false;
2855+
}
2856+
for (let i = 0; i < a.length; i++) {
2857+
const aRect = a[i];
2858+
const bRect = b[i];
2859+
if (
2860+
aRect.x !== bRect.x ||
2861+
aRect.y !== bRect.y ||
2862+
aRect.width !== bRect.width ||
2863+
aRect.height !== bRect.height
2864+
) {
2865+
return false;
2866+
}
2867+
}
2868+
return true;
2869+
}
2870+
2871+
function measureUnchangedSuspenseNodesRecursively(
2872+
suspenseNode: SuspenseNode,
2873+
): void {
2874+
if (isInDisconnectedSubtree) {
2875+
// We don't update rects inside disconnected subtrees.
2876+
return;
2877+
}
2878+
const nextRects = measureInstance(suspenseNode.instance);
2879+
const prevRects = suspenseNode.rects;
2880+
if (areEqualRects(prevRects, nextRects)) {
2881+
return; // Unchanged
2882+
}
2883+
// The rect has changed. While the bailed out root wasn't in a disconnected subtree,
2884+
// it's possible that this node was in one. So we need to check if we're offscreen.
2885+
let parent = suspenseNode.instance.parent;
2886+
while (parent !== null) {
2887+
if (
2888+
(parent.kind === FIBER_INSTANCE ||
2889+
parent.kind === FILTERED_FIBER_INSTANCE) &&
2890+
parent.data.tag === OffscreenComponent &&
2891+
parent.data.memoizedState !== null
2892+
) {
2893+
// We're inside a hidden offscreen Fiber. We're in a disconnected tree.
2894+
return;
2895+
}
2896+
if (parent.suspenseNode !== null) {
2897+
// Found our parent SuspenseNode. We can bail out now.
2898+
break;
2899+
}
2900+
parent = parent.parent;
2901+
}
2902+
// We changed inside a visible tree.
2903+
// Since this boundary changed, it's possible it also affected its children so lets
2904+
// measure them as well.
2905+
for (
2906+
let child = suspenseNode.firstChild;
2907+
child !== null;
2908+
child = child.nextSibling
2909+
) {
2910+
measureUnchangedSuspenseNodesRecursively(child);
2911+
}
2912+
suspenseNode.rects = nextRects;
2913+
recordSuspenseResize(suspenseNode);
2914+
}
2915+
27712916
function consumeSuspenseNodesOfExistingInstance(
27722917
instance: DevToolsInstance,
27732918
): void {
@@ -2806,6 +2951,9 @@ export function attach(
28062951
previouslyReconciledSiblingSuspenseNode.nextSibling = suspenseNode;
28072952
}
28082953
previouslyReconciledSiblingSuspenseNode = suspenseNode;
2954+
// While React didn't rerender this node, it's possible that it was affected by
2955+
// layout due to mutation of a parent or sibling. Check if it changed size.
2956+
measureUnchangedSuspenseNodesRecursively(suspenseNode);
28092957
// Continue
28102958
suspenseNode = nextRemainingSibling;
28112959
} else if (foundOne) {
@@ -3029,6 +3177,10 @@ export function attach(
30293177
newInstance = recordMount(fiber, reconcilingParent);
30303178
if (fiber.tag === SuspenseComponent || fiber.tag === HostRoot) {
30313179
newSuspenseNode = createSuspenseNode(newInstance);
3180+
// Measure this Suspense node. In general we shouldn't do this until we have
3181+
// inserted the new children but since we know this is a FiberInstance we'll
3182+
// just use the Fiber anyway.
3183+
newSuspenseNode.rects = measureInstance(newInstance);
30323184
}
30333185
insertChild(newInstance);
30343186
if (__DEBUG__) {
@@ -3058,6 +3210,10 @@ export function attach(
30583210
newInstance = createFilteredFiberInstance(fiber);
30593211
if (fiber.tag === SuspenseComponent) {
30603212
newSuspenseNode = createSuspenseNode(newInstance);
3213+
// Measure this Suspense node. In general we shouldn't do this until we have
3214+
// inserted the new children but since we know this is a FiberInstance we'll
3215+
// just use the Fiber anyway.
3216+
newSuspenseNode.rects = measureInstance(newInstance);
30613217
}
30623218
insertChild(newInstance);
30633219
if (__DEBUG__) {
@@ -4084,6 +4240,23 @@ export function attach(
40844240
) {
40854241
shouldResetChildren = true;
40864242
}
4243+
} else if (
4244+
nextFiber.memoizedState === null &&
4245+
fiberInstance.suspenseNode !== null
4246+
) {
4247+
if (!isInDisconnectedSubtree) {
4248+
// Measure this Suspense node in case it changed. We don't update the rect while
4249+
// we're inside a disconnected subtree nor if we are the Suspense boundary that
4250+
// is suspended. This lets us keep the rectangle of the displayed content while
4251+
// we're suspended to visualize the resulting state.
4252+
const suspenseNode = fiberInstance.suspenseNode;
4253+
const prevRects = suspenseNode.rects;
4254+
const nextRects = measureInstance(fiberInstance);
4255+
if (!areEqualRects(prevRects, nextRects)) {
4256+
suspenseNode.rects = nextRects;
4257+
recordSuspenseResize(suspenseNode);
4258+
}
4259+
}
40874260
}
40884261
} else {
40894262
// Common case: Primary -> Primary.
@@ -4179,6 +4352,21 @@ export function attach(
41794352
previouslyReconciledSibling = stashedPrevious;
41804353
remainingReconcilingChildren = stashedRemaining;
41814354
if (shouldPopSuspenseNode) {
4355+
if (
4356+
!isInDisconnectedSubtree &&
4357+
reconcilingParentSuspenseNode !== null
4358+
) {
4359+
// Measure this Suspense node in case it changed. We don't update the rect
4360+
// while we're inside a disconnected subtree so that we keep the outline
4361+
// as it was before we hid the parent.
4362+
const suspenseNode = reconcilingParentSuspenseNode;
4363+
const prevRects = suspenseNode.rects;
4364+
const nextRects = measureInstance(fiberInstance);
4365+
if (!areEqualRects(prevRects, nextRects)) {
4366+
suspenseNode.rects = nextRects;
4367+
recordSuspenseResize(suspenseNode);
4368+
}
4369+
}
41824370
reconcilingParentSuspenseNode = stashedSuspenseParent;
41834371
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
41844372
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;

0 commit comments

Comments
 (0)