Skip to content

Commit 49e6dee

Browse files
committed
Suspensily committing a prerendered tree (#26434)
Prerendering a tree (i.e. with Offscreen) should not suspend the commit phase, because the content is not yet visible. However, when revealing a prerendered tree, we should suspend the commit phase if resources in the prerendered tree haven't finished loading yet. To do this properly, we need to visit all the visible nodes in the tree that might possibly suspend. This includes nodes in the current tree, because even though they were already "mounted", the resources might not have loaded yet, because we didn't suspend when it was prerendered. We will need to add this capability to the Offscreen component's "manual" mode, too. Something like a `ready()` method that returns a promise that resolves when the tree has fully loaded. Also includes some fixes to #26450. See PR for details. DiffTrain build for [768f965](768f965)
1 parent a8b6d3f commit 49e6dee

18 files changed

+3307
-2355
lines changed

compiled/facebook-www/REVISION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
d12bdcda69afd219f4d91cbd60d6fae2a375d35b
1+
768f965de2d4c6be7f688562ef02382478c82e5b

compiled/facebook-www/React-dev.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ if (
2727
}
2828
"use strict";
2929

30-
var ReactVersion = "18.3.0-www-modern-770d70a2";
30+
var ReactVersion = "18.3.0-www-modern-4cb7601b";
3131

3232
// ATTENTION
3333
// When adding new symbols to this file,

compiled/facebook-www/ReactART-dev.classic.js

Lines changed: 80 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ function _assertThisInitialized(self) {
6969
return self;
7070
}
7171

72-
var ReactVersion = "18.3.0-www-classic-dfeb256e";
72+
var ReactVersion = "18.3.0-www-classic-e4add0e3";
7373

7474
var LegacyRoot = 0;
7575
var ConcurrentRoot = 1;
@@ -543,11 +543,12 @@ var Visibility =
543543
8192;
544544
var StoreConsistency =
545545
/* */
546-
16384; // It's OK to reuse this bit because these flags are mutually exclusive for
546+
16384; // It's OK to reuse these bits because these flags are mutually exclusive for
547547
// different fiber types. We should really be doing this for as many flags as
548548
// possible, because we're about to run out of bits.
549549

550550
var ScheduleRetry = StoreConsistency;
551+
var ShouldSuspendCommit = Visibility;
551552
var LifecycleEffectMask =
552553
Passive$1 | Update | Callback | Ref | Snapshot | StoreConsistency; // Union of all commit flags (flags with the lifetime of a particular commit)
553554

@@ -587,8 +588,8 @@ var LayoutStatic =
587588
var PassiveStatic =
588589
/* */
589590
8388608;
590-
var SuspenseyCommit =
591-
/* */
591+
var MaySuspendCommit =
592+
/* */
592593
16777216; // Flag used to identify newly inserted fibers. It isn't reset after commit unlike `Placement`.
593594

594595
var PlacementDEV =
@@ -624,7 +625,7 @@ var PassiveMask = Passive$1 | Visibility | ChildDeletion; // Union of tags that
624625
// This allows certain concepts to persist without recalculating them,
625626
// e.g. whether a subtree contains passive effects or portals.
626627

627-
var StaticMask = LayoutStatic | PassiveStatic | RefStatic | SuspenseyCommit;
628+
var StaticMask = LayoutStatic | PassiveStatic | RefStatic | MaySuspendCommit;
628629

629630
var ReactCurrentOwner$2 = ReactSharedInternals.ReactCurrentOwner;
630631
function getNearestMountedFiber(fiber) {
@@ -2934,9 +2935,6 @@ function unhideTextInstance(textInstance, text) {
29342935
function getInstanceFromNode(node) {
29352936
throw new Error("Not implemented.");
29362937
}
2937-
function maySuspendCommit(type, props) {
2938-
return false;
2939-
}
29402938
function preloadInstance(type, props) {
29412939
// Return true to indicate it's already loaded
29422940
return true;
@@ -5590,13 +5588,6 @@ function trackUsedThenable(thenableState, thenable, index) {
55905588
}
55915589
}
55925590
}
5593-
function suspendCommit() {
5594-
// This extra indirection only exists so it can handle passing
5595-
// noopSuspenseyCommitThenable through to throwException.
5596-
// TODO: Factor the thenable check out of throwException
5597-
suspendedThenable = noopSuspenseyCommitThenable;
5598-
throw SuspenseyCommitException;
5599-
} // This is used to track the actual thenable that suspended so it can be
56005591
// passed to the rest of the Suspense implementation — which, for historical
56015592
// reasons, expects to receive a thenable.
56025593

@@ -17697,7 +17688,11 @@ function updateHostComponent(
1769717688
markUpdate(workInProgress);
1769817689
}
1769917690
}
17700-
} // TODO: This should ideally move to begin phase, but currently the instance is
17691+
} // This function must be called at the very end of the complete phase, because
17692+
// it might throw to suspend, and if the resource immediately loads, the work
17693+
// loop will resume rendering as if the work-in-progress completed. So it must
17694+
// fully complete.
17695+
// TODO: This should ideally move to begin phase, but currently the instance is
1770117696
// not created until the complete phase. For our existing use cases, host nodes
1770217697
// that suspend don't have children, so it doesn't matter. But that might not
1770317698
// always be true in the future.
@@ -17708,28 +17703,16 @@ function preloadInstanceAndSuspendIfNeeded(
1770817703
props,
1770917704
renderLanes
1771017705
) {
17711-
workInProgress.flags |= SuspenseyCommit; // Check if we're rendering at a "non-urgent" priority. This is the same
17712-
// check that `useDeferredValue` does to determine whether it needs to
17713-
// defer. This is partly for gradual adoption purposes (i.e. shouldn't start
17714-
// suspending until you opt in with startTransition or Suspense) but it
17715-
// also happens to be the desired behavior for the concrete use cases we've
17716-
// thought of so far, like CSS loading, fonts, images, etc.
17717-
// TODO: We may decide to expose a way to force a fallback even during a
17718-
// sync update.
17719-
17720-
if (!includesOnlyNonUrgentLanes(renderLanes));
17721-
else {
17722-
// Preload the instance
17723-
var isReady = preloadInstance();
17724-
17725-
if (!isReady) {
17726-
if (shouldRemainOnPreviousScreen());
17727-
else {
17728-
// Trigger a fallback rather than block the render.
17729-
suspendCommit();
17730-
}
17731-
}
17732-
}
17706+
{
17707+
// If this flag was set previously, we can remove it. The flag
17708+
// represents whether this particular set of props might ever need to
17709+
// suspend. The safest thing to do is for maySuspendCommit to always
17710+
// return true, but if the renderer is reasonably confident that the
17711+
// underlying resource won't be evicted, it can return false as a
17712+
// performance optimization.
17713+
workInProgress.flags &= ~MaySuspendCommit;
17714+
return;
17715+
} // Mark this fiber with a flag. This gets set on all host instances
1773317716
}
1773417717

1773517718
function scheduleRetryEffect(workInProgress, retryQueue) {
@@ -18160,12 +18143,10 @@ function completeWork(current, workInProgress, renderLanes) {
1816018143

1816118144
case HostComponent: {
1816218145
popHostContext(workInProgress);
18163-
var _type = workInProgress.type;
18164-
18165-
var _maySuspend = maySuspendCommit();
18146+
var _type2 = workInProgress.type;
1816618147

1816718148
if (current !== null && workInProgress.stateNode != null) {
18168-
updateHostComponent(current, workInProgress, _type, newProps);
18149+
updateHostComponent(current, workInProgress, _type2, newProps);
1816918150

1817018151
if (current.ref !== workInProgress.ref) {
1817118152
markRef(workInProgress);
@@ -18201,7 +18182,7 @@ function completeWork(current, workInProgress, renderLanes) {
1820118182
} else {
1820218183
getRootHostContainer();
1820318184

18204-
var _instance3 = createInstance(_type, newProps);
18185+
var _instance3 = createInstance(_type2, newProps);
1820518186

1820618187
appendAllChildren(_instance3, workInProgress);
1820718188
workInProgress.stateNode = _instance3; // Certain renderers require commit-time effects for initial mount.
@@ -18218,17 +18199,7 @@ function completeWork(current, workInProgress, renderLanes) {
1821818199
// will resume rendering as if the work-in-progress completed. So it must
1821918200
// fully complete.
1822018201

18221-
if (_maySuspend) {
18222-
preloadInstanceAndSuspendIfNeeded(
18223-
workInProgress,
18224-
_type,
18225-
newProps,
18226-
renderLanes
18227-
);
18228-
} else {
18229-
workInProgress.flags &= ~SuspenseyCommit;
18230-
}
18231-
18202+
preloadInstanceAndSuspendIfNeeded(workInProgress);
1823218203
return null;
1823318204
}
1823418205

@@ -22603,13 +22574,24 @@ function commitPassiveUnmountEffects(finishedWork) {
2260322574
setCurrentFiber(finishedWork);
2260422575
commitPassiveUnmountOnFiber(finishedWork);
2260522576
resetCurrentFiber();
22606-
}
22577+
} // If we're inside a brand new tree, or a tree that was already visible, then we
22578+
// should only suspend host components that have a ShouldSuspendCommit flag.
22579+
// Components without it haven't changed since the last commit, so we can skip
22580+
// over those.
22581+
//
22582+
// When we enter a tree that is being revealed (going from hidden -> visible),
22583+
// we need to suspend _any_ component that _may_ suspend. Even if they're
22584+
// already in the "current" tree. Because their visibility has changed, the
22585+
// browser may not have prerendered them yet. So we check the MaySuspendCommit
22586+
// flag instead.
22587+
22588+
var suspenseyCommitFlag = ShouldSuspendCommit;
2260722589
function accumulateSuspenseyCommit(finishedWork) {
2260822590
accumulateSuspenseyCommitOnFiber(finishedWork);
2260922591
}
2261022592

2261122593
function recursivelyAccumulateSuspenseyCommit(parentFiber) {
22612-
if (parentFiber.subtreeFlags & SuspenseyCommit) {
22594+
if (parentFiber.subtreeFlags & suspenseyCommitFlag) {
2261322595
var child = parentFiber.child;
2261422596

2261522597
while (child !== null) {
@@ -22624,7 +22606,7 @@ function accumulateSuspenseyCommitOnFiber(fiber) {
2262422606
case HostHoistable: {
2262522607
recursivelyAccumulateSuspenseyCommit(fiber);
2262622608

22627-
if (fiber.flags & SuspenseyCommit) {
22609+
if (fiber.flags & suspenseyCommitFlag) {
2262822610
if (fiber.memoizedState !== null) {
2262922611
suspendResource();
2263022612
}
@@ -22640,8 +22622,36 @@ function accumulateSuspenseyCommitOnFiber(fiber) {
2264022622
}
2264122623

2264222624
case HostRoot:
22643-
case HostPortal:
22644-
// eslint-disable-next-line-no-fallthrough
22625+
case HostPortal: {
22626+
{
22627+
recursivelyAccumulateSuspenseyCommit(fiber);
22628+
}
22629+
22630+
break;
22631+
}
22632+
22633+
case OffscreenComponent: {
22634+
var isHidden = fiber.memoizedState !== null;
22635+
22636+
if (isHidden);
22637+
else {
22638+
var current = fiber.alternate;
22639+
var wasHidden = current !== null && current.memoizedState !== null;
22640+
22641+
if (wasHidden) {
22642+
// This tree is being revealed. Visit all newly visible suspensey
22643+
// instances, even if they're in the current tree.
22644+
var prevFlags = suspenseyCommitFlag;
22645+
suspenseyCommitFlag = MaySuspendCommit;
22646+
recursivelyAccumulateSuspenseyCommit(fiber);
22647+
suspenseyCommitFlag = prevFlags;
22648+
} else {
22649+
recursivelyAccumulateSuspenseyCommit(fiber);
22650+
}
22651+
}
22652+
22653+
break;
22654+
}
2264522655

2264622656
default: {
2264722657
recursivelyAccumulateSuspenseyCommit(fiber);
@@ -24707,15 +24717,24 @@ function shouldRemainOnPreviousScreen() {
2470724717

2470824718
if (handler === null);
2470924719
else {
24710-
if (includesOnlyRetries(workInProgressRootRenderLanes)) {
24720+
if (
24721+
includesOnlyRetries(workInProgressRootRenderLanes) || // In this context, an OffscreenLane counts as a Retry
24722+
// TODO: It's become increasingly clear that Retries and Offscreen are
24723+
// deeply connected. They probably can be unified further.
24724+
includesSomeLane(workInProgressRootRenderLanes, OffscreenLane)
24725+
) {
2471124726
// During a retry, we can suspend rendering if the nearest Suspense boundary
2471224727
// is the boundary of the "shell", because we're guaranteed not to block
2471324728
// any new content from appearing.
24729+
//
24730+
// The reason we must check if this is a retry is because it guarantees
24731+
// that suspending the work loop won't block an actual update, because
24732+
// retries don't "update" anything; they fill in fallbacks that were left
24733+
// behind by a previous transition.
2471424734
return handler === getShellBoundary();
2471524735
}
2471624736
} // For all other Lanes besides Transitions and Retries, we should not wait
2471724737
// for the data to load.
24718-
// TODO: We should wait during Offscreen prerendering, too.
2471924738

2472024739
return false;
2472124740
}

0 commit comments

Comments
 (0)