Skip to content

Commit 409d7c8

Browse files
committed
Track nearest Suspense handler on stack
Instead of traversing the return path whenever something suspends to find the nearest Suspense boundary, we can push the Suspense boundary onto the stack before entering its subtree. This doesn't affect the overall algorithm that much, but because we already do all the same logic in the begin phase, we can save some redundant work by tracking that information on the stack instead of recomputing it every time.
1 parent fcb99a2 commit 409d7c8

14 files changed

+394
-400
lines changed

packages/react-reconciler/src/ReactFiberBeginWork.new.js

Lines changed: 62 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import type {
3636
import type {UpdateQueue} from './ReactUpdateQueue.new';
3737
import type {RootState} from './ReactFiberRoot.new';
3838
import {
39-
enableSuspenseAvoidThisFallback,
4039
enableCPUSuspense,
4140
enableUseMutableSource,
4241
} from 'shared/ReactFeatureFlags';
@@ -166,13 +165,14 @@ import {shouldError, shouldSuspend} from './ReactFiberReconciler';
166165
import {pushHostContext, pushHostContainer} from './ReactFiberHostContext.new';
167166
import {
168167
suspenseStackCursor,
169-
pushSuspenseContext,
170-
InvisibleParentSuspenseContext,
168+
pushSuspenseListContext,
171169
ForceSuspenseFallback,
172-
hasSuspenseContext,
173-
setDefaultShallowSuspenseContext,
174-
addSubtreeSuspenseContext,
175-
setShallowSuspenseContext,
170+
hasSuspenseListContext,
171+
setDefaultShallowSuspenseListContext,
172+
setShallowSuspenseListContext,
173+
pushPrimaryTreeSuspenseHandler,
174+
pushFallbackTreeSuspenseHandler,
175+
popSuspenseHandler,
176176
} from './ReactFiberSuspenseContext.new';
177177
import {
178178
pushHiddenContext,
@@ -1940,7 +1940,6 @@ function updateSuspenseOffscreenState(
19401940

19411941
// TODO: Probably should inline this back
19421942
function shouldRemainOnFallback(
1943-
suspenseContext: SuspenseContext,
19441943
current: null | Fiber,
19451944
workInProgress: Fiber,
19461945
renderLanes: Lanes,
@@ -1960,7 +1959,8 @@ function shouldRemainOnFallback(
19601959
}
19611960

19621961
// Not currently showing content. Consult the Suspense context.
1963-
return hasSuspenseContext(
1962+
const suspenseContext: SuspenseContext = suspenseStackCursor.current;
1963+
return hasSuspenseListContext(
19641964
suspenseContext,
19651965
(ForceSuspenseFallback: SuspenseContext),
19661966
);
@@ -1981,50 +1981,18 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
19811981
}
19821982
}
19831983

1984-
let suspenseContext: SuspenseContext = suspenseStackCursor.current;
1985-
19861984
let showFallback = false;
19871985
const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags;
1988-
19891986
if (
19901987
didSuspend ||
1991-
shouldRemainOnFallback(
1992-
suspenseContext,
1993-
current,
1994-
workInProgress,
1995-
renderLanes,
1996-
)
1988+
shouldRemainOnFallback(current, workInProgress, renderLanes)
19971989
) {
19981990
// Something in this boundary's subtree already suspended. Switch to
19991991
// rendering the fallback children.
20001992
showFallback = true;
20011993
workInProgress.flags &= ~DidCapture;
2002-
} else {
2003-
// Attempting the main content
2004-
if (
2005-
current === null ||
2006-
(current.memoizedState: null | SuspenseState) !== null
2007-
) {
2008-
// This is a new mount or this boundary is already showing a fallback state.
2009-
// Mark this subtree context as having at least one invisible parent that could
2010-
// handle the fallback state.
2011-
// Avoided boundaries are not considered since they cannot handle preferred fallback states.
2012-
if (
2013-
!enableSuspenseAvoidThisFallback ||
2014-
nextProps.unstable_avoidThisFallback !== true
2015-
) {
2016-
suspenseContext = addSubtreeSuspenseContext(
2017-
suspenseContext,
2018-
InvisibleParentSuspenseContext,
2019-
);
2020-
}
2021-
}
20221994
}
20231995

2024-
suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
2025-
2026-
pushSuspenseContext(workInProgress, suspenseContext);
2027-
20281996
// OK, the next part is confusing. We're about to reconcile the Suspense
20291997
// boundary's children. This involves some custom reconciliation logic. Two
20301998
// main reasons this is so complicated.
@@ -2052,24 +2020,40 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
20522020

20532021
// Special path for hydration
20542022
// If we're currently hydrating, try to hydrate this boundary.
2055-
tryToClaimNextHydratableInstance(workInProgress);
2056-
// This could've been a dehydrated suspense component.
2057-
const suspenseState: null | SuspenseState = workInProgress.memoizedState;
2058-
if (suspenseState !== null) {
2059-
const dehydrated = suspenseState.dehydrated;
2060-
if (dehydrated !== null) {
2061-
return mountDehydratedSuspenseComponent(
2062-
workInProgress,
2063-
dehydrated,
2064-
renderLanes,
2065-
);
2023+
if (getIsHydrating()) {
2024+
// We must push the suspense handler context *before* attempting to
2025+
// hydrate, to avoid a mismatch in case it errors.
2026+
if (showFallback) {
2027+
pushPrimaryTreeSuspenseHandler(workInProgress);
2028+
} else {
2029+
pushFallbackTreeSuspenseHandler(workInProgress);
2030+
}
2031+
tryToClaimNextHydratableInstance(workInProgress);
2032+
// This could've been a dehydrated suspense component.
2033+
const suspenseState: null | SuspenseState = workInProgress.memoizedState;
2034+
if (suspenseState !== null) {
2035+
const dehydrated = suspenseState.dehydrated;
2036+
if (dehydrated !== null) {
2037+
return mountDehydratedSuspenseComponent(
2038+
workInProgress,
2039+
dehydrated,
2040+
renderLanes,
2041+
);
2042+
}
20662043
}
2044+
// If hydration didn't succeed, fall through to the normal Suspense path.
2045+
// To avoid a stack mismatch we need to pop the Suspense handler that we
2046+
// pushed above. This will become less awkward when move the hydration
2047+
// logic to its own fiber.
2048+
popSuspenseHandler(workInProgress);
20672049
}
20682050

20692051
const nextPrimaryChildren = nextProps.children;
20702052
const nextFallbackChildren = nextProps.fallback;
20712053

20722054
if (showFallback) {
2055+
pushFallbackTreeSuspenseHandler(workInProgress);
2056+
20732057
const fallbackFragment = mountSuspenseFallbackChildren(
20742058
workInProgress,
20752059
nextPrimaryChildren,
@@ -2099,6 +2083,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
20992083
// This is a CPU-bound tree. Skip this tree and show a placeholder to
21002084
// unblock the surrounding content. Then immediately retry after the
21012085
// initial commit.
2086+
pushFallbackTreeSuspenseHandler(workInProgress);
21022087
const fallbackFragment = mountSuspenseFallbackChildren(
21032088
workInProgress,
21042089
nextPrimaryChildren,
@@ -2122,6 +2107,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
21222107
workInProgress.lanes = SomeRetryLane;
21232108
return fallbackFragment;
21242109
} else {
2110+
pushPrimaryTreeSuspenseHandler(workInProgress);
21252111
return mountSuspensePrimaryChildren(
21262112
workInProgress,
21272113
nextPrimaryChildren,
@@ -2149,6 +2135,8 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
21492135
}
21502136

21512137
if (showFallback) {
2138+
pushFallbackTreeSuspenseHandler(workInProgress);
2139+
21522140
const nextFallbackChildren = nextProps.fallback;
21532141
const nextPrimaryChildren = nextProps.children;
21542142
const fallbackChildFragment = updateSuspenseFallbackChildren(
@@ -2181,6 +2169,8 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
21812169
workInProgress.memoizedState = SUSPENDED_MARKER;
21822170
return fallbackChildFragment;
21832171
} else {
2172+
pushPrimaryTreeSuspenseHandler(workInProgress);
2173+
21842174
const nextPrimaryChildren = nextProps.children;
21852175
const primaryChildFragment = updateSuspensePrimaryChildren(
21862176
current,
@@ -2551,6 +2541,7 @@ function updateDehydratedSuspenseComponent(
25512541
): null | Fiber {
25522542
if (!didSuspend) {
25532543
// This is the first render pass. Attempt to hydrate.
2544+
pushPrimaryTreeSuspenseHandler(workInProgress);
25542545

25552546
// We should never be hydrating at this point because it is the first pass,
25562547
// but after we've already committed once.
@@ -2694,6 +2685,8 @@ function updateDehydratedSuspenseComponent(
26942685

26952686
if (workInProgress.flags & ForceClientRender) {
26962687
// Something errored during hydration. Try again without hydrating.
2688+
pushPrimaryTreeSuspenseHandler(workInProgress);
2689+
26972690
workInProgress.flags &= ~ForceClientRender;
26982691
return retrySuspenseComponentWithoutHydrating(
26992692
current,
@@ -2707,6 +2700,10 @@ function updateDehydratedSuspenseComponent(
27072700
} else if ((workInProgress.memoizedState: null | SuspenseState) !== null) {
27082701
// Something suspended and we should still be in dehydrated mode.
27092702
// Leave the existing child in place.
2703+
2704+
// Push to avoid a mismatch
2705+
pushFallbackTreeSuspenseHandler(workInProgress);
2706+
27102707
workInProgress.child = current.child;
27112708
// The dehydrated completion pass expects this flag to be there
27122709
// but the normal suspense pass doesn't.
@@ -2715,6 +2712,8 @@ function updateDehydratedSuspenseComponent(
27152712
} else {
27162713
// Suspended but we should no longer be in dehydrated mode.
27172714
// Therefore we now have to render the fallback.
2715+
pushFallbackTreeSuspenseHandler(workInProgress);
2716+
27182717
const nextPrimaryChildren = nextProps.children;
27192718
const nextFallbackChildren = nextProps.fallback;
27202719
const fallbackChildFragment = mountSuspenseFallbackAfterRetryWithoutHydrating(
@@ -3010,12 +3009,12 @@ function updateSuspenseListComponent(
30103009

30113010
let suspenseContext: SuspenseContext = suspenseStackCursor.current;
30123011

3013-
const shouldForceFallback = hasSuspenseContext(
3012+
const shouldForceFallback = hasSuspenseListContext(
30143013
suspenseContext,
30153014
(ForceSuspenseFallback: SuspenseContext),
30163015
);
30173016
if (shouldForceFallback) {
3018-
suspenseContext = setShallowSuspenseContext(
3017+
suspenseContext = setShallowSuspenseListContext(
30193018
suspenseContext,
30203019
ForceSuspenseFallback,
30213020
);
@@ -3033,9 +3032,9 @@ function updateSuspenseListComponent(
30333032
renderLanes,
30343033
);
30353034
}
3036-
suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
3035+
suspenseContext = setDefaultShallowSuspenseListContext(suspenseContext);
30373036
}
3038-
pushSuspenseContext(workInProgress, suspenseContext);
3037+
pushSuspenseListContext(workInProgress, suspenseContext);
30393038

30403039
if ((workInProgress.mode & ConcurrentMode) === NoMode) {
30413040
// In legacy mode, SuspenseList doesn't work so we just
@@ -3499,10 +3498,9 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
34993498
const state: SuspenseState | null = workInProgress.memoizedState;
35003499
if (state !== null) {
35013500
if (state.dehydrated !== null) {
3502-
pushSuspenseContext(
3503-
workInProgress,
3504-
setDefaultShallowSuspenseContext(suspenseStackCursor.current),
3505-
);
3501+
// We're not going to render the children, so this is just to maintain
3502+
// push/pop symmetry
3503+
pushPrimaryTreeSuspenseHandler(workInProgress);
35063504
// We know that this component will suspend again because if it has
35073505
// been unsuspended it has committed as a resolved Suspense component.
35083506
// If it needs to be retried, it should have work scheduled on it.
@@ -3525,10 +3523,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
35253523
} else {
35263524
// The primary child fragment does not have pending work marked
35273525
// on it
3528-
pushSuspenseContext(
3529-
workInProgress,
3530-
setDefaultShallowSuspenseContext(suspenseStackCursor.current),
3531-
);
3526+
pushPrimaryTreeSuspenseHandler(workInProgress);
35323527
// The primary children do not have pending work with sufficient
35333528
// priority. Bailout.
35343529
const child = bailoutOnAlreadyFinishedWork(
@@ -3548,10 +3543,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
35483543
}
35493544
}
35503545
} else {
3551-
pushSuspenseContext(
3552-
workInProgress,
3553-
setDefaultShallowSuspenseContext(suspenseStackCursor.current),
3554-
);
3546+
pushPrimaryTreeSuspenseHandler(workInProgress);
35553547
}
35563548
break;
35573549
}
@@ -3609,7 +3601,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
36093601
renderState.tail = null;
36103602
renderState.lastEffect = null;
36113603
}
3612-
pushSuspenseContext(workInProgress, suspenseStackCursor.current);
3604+
pushSuspenseListContext(workInProgress, suspenseStackCursor.current);
36133605

36143606
if (hasChildWork) {
36153607
break;

0 commit comments

Comments
 (0)