@samitouri / QOS-React-2 / commits / ba4bdb2ab5

[DevTools] Consume SuspenseNodes that were skipped when we're bailing out of a subtree (#34082)

This searches through the remaining children to see if any of them were children of the bailed out FiberInstance and if so we should reuse them in the new set. It's faster to do this than search through children of the FiberInstance for Suspense boundaries.

Sebastian Markbåge committed Aug 4, 2025 at 13:04 UTC ba4bdb2ab5e15398fb2ffaafe944a1de56851734
1 file changed +87 -9
packages/react-devtools-shared/src/backend/fiber/renderer.js
+87 -9
@@ -2701,6 +2701,76 @@ export function attach(
2701 }
2702 }
2703
2704 + function isChildOf(
2705 + parentInstance: DevToolsInstance,
2706 + childInstance: DevToolsInstance,
2707 + grandParent: DevToolsInstance,
2708 + ): boolean {
2709 + let instance = childInstance.parent;
2710 + while (instance !== null) {
2711 + if (parentInstance === instance) {
2712 + return true;
2713 + }
2714 + if (instance === parentInstance.parent || instance === grandParent) {
2715 + // This was a sibling but not inside the FiberInstance. We can bail out.
2716 + break;
2717 + }
2718 + instance = instance.parent;
2719 + }
2720 + return false;
2721 + }
2722 +
2723 + function consumeSuspenseNodesOfExistingInstance(
2724 + instance: DevToolsInstance,
2725 + ): void {
2726 + // We need to also consume any unchanged Suspense boundaries.
2727 + let suspenseNode = remainingReconcilingChildrenSuspenseNodes;
2728 + if (suspenseNode === null) {
2729 + return;
2730 + }
2731 + const parentSuspenseNode = reconcilingParentSuspenseNode;
2732 + if (parentSuspenseNode === null) {
2733 + throw new Error(
2734 + 'The should not be any remaining suspense node children if there is no parent.',
2735 + );
2736 + }
2737 + let foundOne = false;
2738 + let previousSkippedSibling = null;
2739 + while (suspenseNode !== null) {
2740 + // Check if this SuspenseNode was a child of the bailed out FiberInstance.
2741 + if (
2742 + isChildOf(instance, suspenseNode.instance, parentSuspenseNode.instance)
2743 + ) {
2744 + foundOne = true;
2745 + // The suspenseNode was child of the bailed out Fiber.
2746 + // First, remove it from the remaining children set.
2747 + const nextRemainingSibling = suspenseNode.nextSibling;
2748 + if (previousSkippedSibling === null) {
2749 + remainingReconcilingChildrenSuspenseNodes = nextRemainingSibling;
2750 + } else {
2751 + previousSkippedSibling.nextSibling = nextRemainingSibling;
2752 + }
2753 + suspenseNode.nextSibling = null;
2754 + // Then, re-insert it into the newly reconciled set.
2755 + if (previouslyReconciledSiblingSuspenseNode === null) {
2756 + parentSuspenseNode.firstChild = suspenseNode;
2757 + } else {
2758 + previouslyReconciledSiblingSuspenseNode.nextSibling = suspenseNode;
2759 + }
2760 + previouslyReconciledSiblingSuspenseNode = suspenseNode;
2761 + // Continue
2762 + suspenseNode = nextRemainingSibling;
2763 + } else if (foundOne) {
2764 + // If we found one and then hit a miss, we assume that we're passed the sequence because
2765 + // they should've all been consecutive.
2766 + break;
2767 + } else {
2768 + previousSkippedSibling = suspenseNode;
2769 + suspenseNode = suspenseNode.nextSibling;
2770 + }
2771 + }
2772 + }
2773 +
2774 function mountVirtualInstanceRecursively(
2775 virtualInstance: VirtualInstance,
2776 firstChild: Fiber,
@@ -3094,9 +3164,11 @@ export function attach(
3164 reconcilingParent = stashedParent;
3165 previouslyReconciledSibling = stashedPrevious;
3166 remainingReconcilingChildren = stashedRemaining;
3097 - reconcilingParentSuspenseNode = stashedSuspenseParent;
3098 - previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
3099 - remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
3167 + if (instance.suspenseNode !== null) {
3168 + reconcilingParentSuspenseNode = stashedSuspenseParent;
3169 + previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
3170 + remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
3171 + }
3172 }
3173 if (instance.kind === FIBER_INSTANCE) {
3174 recordUnmount(instance);
@@ -3688,10 +3760,12 @@ export function attach(
3760 fiberInstance.firstChild = null;
3761 fiberInstance.suspendedBy = null;
3762
3691 - if (fiberInstance.suspenseNode !== null) {
3692 - reconcilingParentSuspenseNode = fiberInstance.suspenseNode;
3763 + const suspenseNode = fiberInstance.suspenseNode;
3764 + if (suspenseNode !== null) {
3765 + reconcilingParentSuspenseNode = suspenseNode;
3766 previouslyReconciledSiblingSuspenseNode = null;
3694 - remainingReconcilingChildrenSuspenseNodes = null;
3767 + remainingReconcilingChildrenSuspenseNodes = suspenseNode.firstChild;
3768 + suspenseNode.firstChild = null;
3769 }
3770 }
3771 try {
@@ -3849,6 +3923,8 @@ export function attach(
3923 fiberInstance.firstChild = remainingReconcilingChildren;
3924 remainingReconcilingChildren = null;
3925
3926 + consumeSuspenseNodesOfExistingInstance(fiberInstance);
3927 +
3928 if (traceUpdatesEnabled) {
3929 // If we're tracing updates and we've bailed out before reaching a host node,
3930 // we should fall back to recursively marking the nearest host descendants for highlight.
@@ -3919,9 +3995,11 @@ export function attach(
3995 reconcilingParent = stashedParent;
3996 previouslyReconciledSibling = stashedPrevious;
3997 remainingReconcilingChildren = stashedRemaining;
3922 - reconcilingParentSuspenseNode = stashedSuspenseParent;
3923 - previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
3924 - remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
3998 + if (fiberInstance.suspenseNode !== null) {
3999 + reconcilingParentSuspenseNode = stashedSuspenseParent;
4000 + previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
4001 + remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
4002 + }
4003 }
4004 }
4005 }