Skip to content

Commit 6ab05ee

Browse files
committed
[FORKED] 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 a60ae2e commit 6ab05ee

7 files changed

+197
-200
lines changed

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

Lines changed: 62 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new';
3737
import type {RootState} from './ReactFiberRoot.new';
3838
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.new';
3939
import {
40-
enableSuspenseAvoidThisFallback,
4140
enableCPUSuspense,
4241
enableUseMutableSource,
4342
} from 'shared/ReactFeatureFlags';
@@ -167,13 +166,14 @@ import {shouldError, shouldSuspend} from './ReactFiberReconciler';
167166
import {pushHostContext, pushHostContainer} from './ReactFiberHostContext.new';
168167
import {
169168
suspenseStackCursor,
170-
pushSuspenseContext,
171-
InvisibleParentSuspenseContext,
169+
pushSuspenseListContext,
172170
ForceSuspenseFallback,
173-
hasSuspenseContext,
174-
setDefaultShallowSuspenseContext,
175-
addSubtreeSuspenseContext,
176-
setShallowSuspenseContext,
171+
hasSuspenseListContext,
172+
setDefaultShallowSuspenseListContext,
173+
setShallowSuspenseListContext,
174+
pushPrimaryTreeSuspenseHandler,
175+
pushFallbackTreeSuspenseHandler,
176+
popSuspenseHandler,
177177
} from './ReactFiberSuspenseContext.new';
178178
import {
179179
pushHiddenContext,
@@ -1969,7 +1969,6 @@ function updateSuspenseOffscreenState(
19691969

19701970
// TODO: Probably should inline this back
19711971
function shouldRemainOnFallback(
1972-
suspenseContext: SuspenseContext,
19731972
current: null | Fiber,
19741973
workInProgress: Fiber,
19751974
renderLanes: Lanes,
@@ -1989,7 +1988,8 @@ function shouldRemainOnFallback(
19891988
}
19901989

19911990
// Not currently showing content. Consult the Suspense context.
1992-
return hasSuspenseContext(
1991+
const suspenseContext: SuspenseContext = suspenseStackCursor.current;
1992+
return hasSuspenseListContext(
19931993
suspenseContext,
19941994
(ForceSuspenseFallback: SuspenseContext),
19951995
);
@@ -2010,50 +2010,18 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
20102010
}
20112011
}
20122012

2013-
let suspenseContext: SuspenseContext = suspenseStackCursor.current;
2014-
20152013
let showFallback = false;
20162014
const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags;
2017-
20182015
if (
20192016
didSuspend ||
2020-
shouldRemainOnFallback(
2021-
suspenseContext,
2022-
current,
2023-
workInProgress,
2024-
renderLanes,
2025-
)
2017+
shouldRemainOnFallback(current, workInProgress, renderLanes)
20262018
) {
20272019
// Something in this boundary's subtree already suspended. Switch to
20282020
// rendering the fallback children.
20292021
showFallback = true;
20302022
workInProgress.flags &= ~DidCapture;
2031-
} else {
2032-
// Attempting the main content
2033-
if (
2034-
current === null ||
2035-
(current.memoizedState: null | SuspenseState) !== null
2036-
) {
2037-
// This is a new mount or this boundary is already showing a fallback state.
2038-
// Mark this subtree context as having at least one invisible parent that could
2039-
// handle the fallback state.
2040-
// Avoided boundaries are not considered since they cannot handle preferred fallback states.
2041-
if (
2042-
!enableSuspenseAvoidThisFallback ||
2043-
nextProps.unstable_avoidThisFallback !== true
2044-
) {
2045-
suspenseContext = addSubtreeSuspenseContext(
2046-
suspenseContext,
2047-
InvisibleParentSuspenseContext,
2048-
);
2049-
}
2050-
}
20512023
}
20522024

2053-
suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
2054-
2055-
pushSuspenseContext(workInProgress, suspenseContext);
2056-
20572025
// OK, the next part is confusing. We're about to reconcile the Suspense
20582026
// boundary's children. This involves some custom reconciliation logic. Two
20592027
// main reasons this is so complicated.
@@ -2081,24 +2049,40 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
20812049

20822050
// Special path for hydration
20832051
// If we're currently hydrating, try to hydrate this boundary.
2084-
tryToClaimNextHydratableInstance(workInProgress);
2085-
// This could've been a dehydrated suspense component.
2086-
const suspenseState: null | SuspenseState = workInProgress.memoizedState;
2087-
if (suspenseState !== null) {
2088-
const dehydrated = suspenseState.dehydrated;
2089-
if (dehydrated !== null) {
2090-
return mountDehydratedSuspenseComponent(
2091-
workInProgress,
2092-
dehydrated,
2093-
renderLanes,
2094-
);
2052+
if (getIsHydrating()) {
2053+
// We must push the suspense handler context *before* attempting to
2054+
// hydrate, to avoid a mismatch in case it errors.
2055+
if (showFallback) {
2056+
pushPrimaryTreeSuspenseHandler(workInProgress);
2057+
} else {
2058+
pushFallbackTreeSuspenseHandler(workInProgress);
2059+
}
2060+
tryToClaimNextHydratableInstance(workInProgress);
2061+
// This could've been a dehydrated suspense component.
2062+
const suspenseState: null | SuspenseState = workInProgress.memoizedState;
2063+
if (suspenseState !== null) {
2064+
const dehydrated = suspenseState.dehydrated;
2065+
if (dehydrated !== null) {
2066+
return mountDehydratedSuspenseComponent(
2067+
workInProgress,
2068+
dehydrated,
2069+
renderLanes,
2070+
);
2071+
}
20952072
}
2073+
// If hydration didn't succeed, fall through to the normal Suspense path.
2074+
// To avoid a stack mismatch we need to pop the Suspense handler that we
2075+
// pushed above. This will become less awkward when move the hydration
2076+
// logic to its own fiber.
2077+
popSuspenseHandler(workInProgress);
20962078
}
20972079

20982080
const nextPrimaryChildren = nextProps.children;
20992081
const nextFallbackChildren = nextProps.fallback;
21002082

21012083
if (showFallback) {
2084+
pushFallbackTreeSuspenseHandler(workInProgress);
2085+
21022086
const fallbackFragment = mountSuspenseFallbackChildren(
21032087
workInProgress,
21042088
nextPrimaryChildren,
@@ -2131,6 +2115,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
21312115
// This is a CPU-bound tree. Skip this tree and show a placeholder to
21322116
// unblock the surrounding content. Then immediately retry after the
21332117
// initial commit.
2118+
pushFallbackTreeSuspenseHandler(workInProgress);
21342119
const fallbackFragment = mountSuspenseFallbackChildren(
21352120
workInProgress,
21362121
nextPrimaryChildren,
@@ -2154,6 +2139,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
21542139
workInProgress.lanes = SomeRetryLane;
21552140
return fallbackFragment;
21562141
} else {
2142+
pushPrimaryTreeSuspenseHandler(workInProgress);
21572143
return mountSuspensePrimaryChildren(
21582144
workInProgress,
21592145
nextPrimaryChildren,
@@ -2181,6 +2167,8 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
21812167
}
21822168

21832169
if (showFallback) {
2170+
pushFallbackTreeSuspenseHandler(workInProgress);
2171+
21842172
const nextFallbackChildren = nextProps.fallback;
21852173
const nextPrimaryChildren = nextProps.children;
21862174
const fallbackChildFragment = updateSuspenseFallbackChildren(
@@ -2215,6 +2203,8 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
22152203
workInProgress.memoizedState = SUSPENDED_MARKER;
22162204
return fallbackChildFragment;
22172205
} else {
2206+
pushPrimaryTreeSuspenseHandler(workInProgress);
2207+
22182208
const nextPrimaryChildren = nextProps.children;
22192209
const primaryChildFragment = updateSuspensePrimaryChildren(
22202210
current,
@@ -2585,6 +2575,7 @@ function updateDehydratedSuspenseComponent(
25852575
): null | Fiber {
25862576
if (!didSuspend) {
25872577
// This is the first render pass. Attempt to hydrate.
2578+
pushPrimaryTreeSuspenseHandler(workInProgress);
25882579

25892580
// We should never be hydrating at this point because it is the first pass,
25902581
// but after we've already committed once.
@@ -2751,6 +2742,8 @@ function updateDehydratedSuspenseComponent(
27512742

27522743
if (workInProgress.flags & ForceClientRender) {
27532744
// Something errored during hydration. Try again without hydrating.
2745+
pushPrimaryTreeSuspenseHandler(workInProgress);
2746+
27542747
workInProgress.flags &= ~ForceClientRender;
27552748
const capturedValue = createCapturedValue(
27562749
new Error(
@@ -2767,6 +2760,10 @@ function updateDehydratedSuspenseComponent(
27672760
} else if ((workInProgress.memoizedState: null | SuspenseState) !== null) {
27682761
// Something suspended and we should still be in dehydrated mode.
27692762
// Leave the existing child in place.
2763+
2764+
// Push to avoid a mismatch
2765+
pushFallbackTreeSuspenseHandler(workInProgress);
2766+
27702767
workInProgress.child = current.child;
27712768
// The dehydrated completion pass expects this flag to be there
27722769
// but the normal suspense pass doesn't.
@@ -2775,6 +2772,8 @@ function updateDehydratedSuspenseComponent(
27752772
} else {
27762773
// Suspended but we should no longer be in dehydrated mode.
27772774
// Therefore we now have to render the fallback.
2775+
pushFallbackTreeSuspenseHandler(workInProgress);
2776+
27782777
const nextPrimaryChildren = nextProps.children;
27792778
const nextFallbackChildren = nextProps.fallback;
27802779
const fallbackChildFragment = mountSuspenseFallbackAfterRetryWithoutHydrating(
@@ -3070,12 +3069,12 @@ function updateSuspenseListComponent(
30703069

30713070
let suspenseContext: SuspenseContext = suspenseStackCursor.current;
30723071

3073-
const shouldForceFallback = hasSuspenseContext(
3072+
const shouldForceFallback = hasSuspenseListContext(
30743073
suspenseContext,
30753074
(ForceSuspenseFallback: SuspenseContext),
30763075
);
30773076
if (shouldForceFallback) {
3078-
suspenseContext = setShallowSuspenseContext(
3077+
suspenseContext = setShallowSuspenseListContext(
30793078
suspenseContext,
30803079
ForceSuspenseFallback,
30813080
);
@@ -3093,9 +3092,9 @@ function updateSuspenseListComponent(
30933092
renderLanes,
30943093
);
30953094
}
3096-
suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
3095+
suspenseContext = setDefaultShallowSuspenseListContext(suspenseContext);
30973096
}
3098-
pushSuspenseContext(workInProgress, suspenseContext);
3097+
pushSuspenseListContext(workInProgress, suspenseContext);
30993098

31003099
if ((workInProgress.mode & ConcurrentMode) === NoMode) {
31013100
// In legacy mode, SuspenseList doesn't work so we just
@@ -3559,10 +3558,9 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
35593558
const state: SuspenseState | null = workInProgress.memoizedState;
35603559
if (state !== null) {
35613560
if (state.dehydrated !== null) {
3562-
pushSuspenseContext(
3563-
workInProgress,
3564-
setDefaultShallowSuspenseContext(suspenseStackCursor.current),
3565-
);
3561+
// We're not going to render the children, so this is just to maintain
3562+
// push/pop symmetry
3563+
pushPrimaryTreeSuspenseHandler(workInProgress);
35663564
// We know that this component will suspend again because if it has
35673565
// been unsuspended it has committed as a resolved Suspense component.
35683566
// If it needs to be retried, it should have work scheduled on it.
@@ -3585,10 +3583,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
35853583
} else {
35863584
// The primary child fragment does not have pending work marked
35873585
// on it
3588-
pushSuspenseContext(
3589-
workInProgress,
3590-
setDefaultShallowSuspenseContext(suspenseStackCursor.current),
3591-
);
3586+
pushPrimaryTreeSuspenseHandler(workInProgress);
35923587
// The primary children do not have pending work with sufficient
35933588
// priority. Bailout.
35943589
const child = bailoutOnAlreadyFinishedWork(
@@ -3608,10 +3603,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
36083603
}
36093604
}
36103605
} else {
3611-
pushSuspenseContext(
3612-
workInProgress,
3613-
setDefaultShallowSuspenseContext(suspenseStackCursor.current),
3614-
);
3606+
pushPrimaryTreeSuspenseHandler(workInProgress);
36153607
}
36163608
break;
36173609
}
@@ -3669,7 +3661,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
36693661
renderState.tail = null;
36703662
renderState.lastEffect = null;
36713663
}
3672-
pushSuspenseContext(workInProgress, suspenseStackCursor.current);
3664+
pushSuspenseListContext(workInProgress, suspenseStackCursor.current);
36733665

36743666
if (hasChildWork) {
36753667
break;

0 commit comments

Comments
 (0)