@samitouri / QOS-React / commits / d8a15c49a4

[Fiber] Reset remaining child lanes after propagating context inside Offscreen (#34658)

Otherwise, when a context is propagated into an Activity (or Suspense) this will leave work behind on the Offscreen component itself. Which will cause an extra unnecessary render and commit pass just to figure out that we're still defering it to idle. This is because lazy context propagation, when calling to schedule some work walks back up the tree all the way to the root. This is usually fine for other nodes since they'll recompute their remaining child lanes on the way up. However, for the Offscreen component we'll have already computed it. We need to set it after propagation to ensure it gets reset.

Sebastian Markbåge committed Sep 30, 2025 at 14:51 UTC d8a15c49a4bac8fb6730737c34eaf9c74c2f0d7e
1 file changed +16 -5
packages/react-reconciler/src/ReactFiberBeginWork.js
+16 -5
@@ -649,6 +649,7 @@ function updateOffscreenComponent(
649 ? mergeLanes(prevState.baseLanes, renderLanes)
650 : renderLanes;
651
652 + let remainingChildLanes;
653 if (current !== null) {
654 // Reset to the current children
655 let currentChild = (workInProgress.child = current.child);
@@ -666,13 +667,12 @@ function updateOffscreenComponent(
667 currentChild = currentChild.sibling;
668 }
669 const lanesWeJustAttempted = nextBaseLanes;
669 - const remainingChildLanes = removeLanes(
670 + remainingChildLanes = removeLanes(
671 currentChildLanes,
672 lanesWeJustAttempted,
673 );
673 - workInProgress.childLanes = remainingChildLanes;
674 } else {
675 - workInProgress.childLanes = NoLanes;
675 + remainingChildLanes = NoLanes;
676 workInProgress.child = null;
677 }
678
@@ -681,6 +681,7 @@ function updateOffscreenComponent(
681 workInProgress,
682 nextBaseLanes,
683 renderLanes,
684 + remainingChildLanes,
685 );
686 }
687
@@ -707,8 +708,9 @@ function updateOffscreenComponent(
708 // and resume this tree later.
709
710 // Schedule this fiber to re-render at Offscreen priority
710 - workInProgress.lanes = workInProgress.childLanes =
711 - laneToLanes(OffscreenLane);
711 +
712 + const remainingChildLanes = (workInProgress.lanes =
713 + laneToLanes(OffscreenLane));
714
715 // Include the base lanes from the last render
716 const nextBaseLanes =
@@ -721,6 +723,7 @@ function updateOffscreenComponent(
723 workInProgress,
724 nextBaseLanes,
725 renderLanes,
726 + remainingChildLanes,
727 );
728 } else {
729 // This is the second render. The surrounding visible content has already
@@ -826,6 +829,7 @@ function deferHiddenOffscreenComponent(
829 workInProgress: Fiber,
830 nextBaseLanes: Lanes,
831 renderLanes: Lanes,
832 + remainingChildLanes: Lanes,
833 ) {
834 const nextState: OffscreenState = {
835 baseLanes: nextBaseLanes,
@@ -856,6 +860,13 @@ function deferHiddenOffscreenComponent(
860 );
861 }
862
863 + // We override the remaining child lanes to be the subset that we computed
864 + // on the outside. We need to do this after propagating the context
865 + // because propagateParentContextChangesToDeferredTree may schedule
866 + // work which bubbles all the way up to the root and updates our child lanes.
867 + // We want to dismiss that since we're not going to work on it yet.
868 + workInProgress.childLanes = remainingChildLanes;
869 +
870 return null;
871 }
872