[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