Skip to content

Commit

Permalink
Suspensily committing a prerendered tree (#26434)
Browse files Browse the repository at this point in the history
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 commit 768f965.
  • Loading branch information
acdlite committed Mar 27, 2023
1 parent e591dc4 commit 7f1afb7
Show file tree
Hide file tree
Showing 13 changed files with 464 additions and 305 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -484,11 +484,12 @@ var Visibility =
8192;
var StoreConsistency =
/* */
16384; // It's OK to reuse this bit because these flags are mutually exclusive for
16384; // It's OK to reuse these bits because these flags are mutually exclusive for
// different fiber types. We should really be doing this for as many flags as
// possible, because we're about to run out of bits.

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

Expand Down Expand Up @@ -522,8 +523,8 @@ var LayoutStatic =
var PassiveStatic =
/* */
8388608;
var SuspenseyCommit =
/* */
var MaySuspendCommit =
/* */
16777216; // Flag used to identify newly inserted fibers. It isn't reset after commit unlike `Placement`.

var PlacementDEV =
Expand Down Expand Up @@ -554,7 +555,7 @@ var PassiveMask = Passive$1 | Visibility | ChildDeletion; // Union of tags that
// This allows certain concepts to persist without recalculating them,
// e.g. whether a subtree contains passive effects or portals.

var StaticMask = LayoutStatic | PassiveStatic | RefStatic | SuspenseyCommit;
var StaticMask = LayoutStatic | PassiveStatic | RefStatic | MaySuspendCommit;

var ReactCurrentOwner$2 = ReactSharedInternals.ReactCurrentOwner;
function getNearestMountedFiber(fiber) {
Expand Down Expand Up @@ -2023,9 +2024,6 @@ function unhideInstance(instance, props) {
function unhideTextInstance(textInstance, text) {
textInstance.isHidden = false;
}
function maySuspendCommit(type, props) {
return false;
}
function preloadInstance(type, props) {
// Return true to indicate it's already loaded
return true;
Expand Down Expand Up @@ -4310,13 +4308,6 @@ function trackUsedThenable(thenableState, thenable, index) {
}
}
}
function suspendCommit() {
// This extra indirection only exists so it can handle passing
// noopSuspenseyCommitThenable through to throwException.
// TODO: Factor the thenable check out of throwException
suspendedThenable = noopSuspenseyCommitThenable;
throw SuspenseyCommitException;
} // This is used to track the actual thenable that suspended so it can be
// passed to the rest of the Suspense implementation — which, for historical
// reasons, expects to receive a thenable.

Expand Down Expand Up @@ -14604,7 +14595,11 @@ function updateHostComponent(
markUpdate(workInProgress);
}
}
} // TODO: This should ideally move to begin phase, but currently the instance is
} // This function must be called at the very end of the complete phase, because
// it might throw to suspend, and if the resource immediately loads, the work
// loop will resume rendering as if the work-in-progress completed. So it must
// fully complete.
// TODO: This should ideally move to begin phase, but currently the instance is
// not created until the complete phase. For our existing use cases, host nodes
// that suspend don't have children, so it doesn't matter. But that might not
// always be true in the future.
Expand All @@ -14615,28 +14610,16 @@ function preloadInstanceAndSuspendIfNeeded(
props,
renderLanes
) {
workInProgress.flags |= SuspenseyCommit; // Check if we're rendering at a "non-urgent" priority. This is the same
// check that `useDeferredValue` does to determine whether it needs to
// defer. This is partly for gradual adoption purposes (i.e. shouldn't start
// suspending until you opt in with startTransition or Suspense) but it
// also happens to be the desired behavior for the concrete use cases we've
// thought of so far, like CSS loading, fonts, images, etc.
// TODO: We may decide to expose a way to force a fallback even during a
// sync update.

if (!includesOnlyNonUrgentLanes(renderLanes));
else {
// Preload the instance
var isReady = preloadInstance();

if (!isReady) {
if (shouldRemainOnPreviousScreen());
else {
// Trigger a fallback rather than block the render.
suspendCommit();
}
}
}
{
// If this flag was set previously, we can remove it. The flag
// represents whether this particular set of props might ever need to
// suspend. The safest thing to do is for maySuspendCommit to always
// return true, but if the renderer is reasonably confident that the
// underlying resource won't be evicted, it can return false as a
// performance optimization.
workInProgress.flags &= ~MaySuspendCommit;
return;
} // Mark this fiber with a flag. This gets set on all host instances
}

function scheduleRetryEffect(workInProgress, retryQueue) {
Expand Down Expand Up @@ -15042,12 +15025,10 @@ function completeWork(current, workInProgress, renderLanes) {

case HostComponent: {
popHostContext(workInProgress);
var _type = workInProgress.type;

var _maySuspend = maySuspendCommit();
var _type2 = workInProgress.type;

if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(current, workInProgress, _type, newProps);
updateHostComponent(current, workInProgress, _type2, newProps);

if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
Expand Down Expand Up @@ -15084,7 +15065,7 @@ function completeWork(current, workInProgress, renderLanes) {
var _rootContainerInstance = getRootHostContainer();

var _instance3 = createInstance(
_type,
_type2,
newProps,
_rootContainerInstance,
_currentHostContext2,
Expand All @@ -15106,17 +15087,7 @@ function completeWork(current, workInProgress, renderLanes) {
// will resume rendering as if the work-in-progress completed. So it must
// fully complete.

if (_maySuspend) {
preloadInstanceAndSuspendIfNeeded(
workInProgress,
_type,
newProps,
renderLanes
);
} else {
workInProgress.flags &= ~SuspenseyCommit;
}

preloadInstanceAndSuspendIfNeeded(workInProgress);
return null;
}

Expand Down Expand Up @@ -18973,13 +18944,24 @@ function commitPassiveUnmountEffects(finishedWork) {
setCurrentFiber(finishedWork);
commitPassiveUnmountOnFiber(finishedWork);
resetCurrentFiber();
}
} // If we're inside a brand new tree, or a tree that was already visible, then we
// should only suspend host components that have a ShouldSuspendCommit flag.
// Components without it haven't changed since the last commit, so we can skip
// over those.
//
// When we enter a tree that is being revealed (going from hidden -> visible),
// we need to suspend _any_ component that _may_ suspend. Even if they're
// already in the "current" tree. Because their visibility has changed, the
// browser may not have prerendered them yet. So we check the MaySuspendCommit
// flag instead.

var suspenseyCommitFlag = ShouldSuspendCommit;
function accumulateSuspenseyCommit(finishedWork) {
accumulateSuspenseyCommitOnFiber(finishedWork);
}

function recursivelyAccumulateSuspenseyCommit(parentFiber) {
if (parentFiber.subtreeFlags & SuspenseyCommit) {
if (parentFiber.subtreeFlags & suspenseyCommitFlag) {
var child = parentFiber.child;

while (child !== null) {
Expand All @@ -18994,7 +18976,7 @@ function accumulateSuspenseyCommitOnFiber(fiber) {
case HostHoistable: {
recursivelyAccumulateSuspenseyCommit(fiber);

if (fiber.flags & SuspenseyCommit) {
if (fiber.flags & suspenseyCommitFlag) {
if (fiber.memoizedState !== null) {
suspendResource();
}
Expand All @@ -19010,8 +18992,36 @@ function accumulateSuspenseyCommitOnFiber(fiber) {
}

case HostRoot:
case HostPortal:
// eslint-disable-next-line-no-fallthrough
case HostPortal: {
{
recursivelyAccumulateSuspenseyCommit(fiber);
}

break;
}

case OffscreenComponent: {
var isHidden = fiber.memoizedState !== null;

if (isHidden);
else {
var current = fiber.alternate;
var wasHidden = current !== null && current.memoizedState !== null;

if (wasHidden) {
// This tree is being revealed. Visit all newly visible suspensey
// instances, even if they're in the current tree.
var prevFlags = suspenseyCommitFlag;
suspenseyCommitFlag = MaySuspendCommit;
recursivelyAccumulateSuspenseyCommit(fiber);
suspenseyCommitFlag = prevFlags;
} else {
recursivelyAccumulateSuspenseyCommit(fiber);
}
}

break;
}

default: {
recursivelyAccumulateSuspenseyCommit(fiber);
Expand Down Expand Up @@ -20792,15 +20802,24 @@ function shouldRemainOnPreviousScreen() {

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

return false;
}
Expand Down Expand Up @@ -23743,7 +23762,7 @@ function createFiberRoot(
return root;
}

var ReactVersion = "18.3.0-next-d12bdcda6-20230325";
var ReactVersion = "18.3.0-next-768f965de-20230326";

// Might add PROFILE later.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6038,8 +6038,9 @@ function recursivelyTraverseAtomicPassiveEffects(
parentFiber = parentFiber.sibling;
}
}
var suspenseyCommitFlag = 8192;
function recursivelyAccumulateSuspenseyCommit(parentFiber) {
if (parentFiber.subtreeFlags & 16777216)
if (parentFiber.subtreeFlags & suspenseyCommitFlag)
for (parentFiber = parentFiber.child; null !== parentFiber; )
accumulateSuspenseyCommitOnFiber(parentFiber),
(parentFiber = parentFiber.sibling);
Expand All @@ -6048,14 +6049,29 @@ function accumulateSuspenseyCommitOnFiber(fiber) {
switch (fiber.tag) {
case 26:
recursivelyAccumulateSuspenseyCommit(fiber);
if (fiber.flags & 16777216 && null !== fiber.memoizedState)
if (fiber.flags & suspenseyCommitFlag && null !== fiber.memoizedState)
throw Error(
"The current renderer does not support Resources. This error is likely caused by a bug in React. Please file an issue."
);
break;
case 5:
recursivelyAccumulateSuspenseyCommit(fiber);
break;
case 3:
case 4:
recursivelyAccumulateSuspenseyCommit(fiber);
break;
case 22:
if (null === fiber.memoizedState) {
var current = fiber.alternate;
null !== current && null !== current.memoizedState
? ((current = suspenseyCommitFlag),
(suspenseyCommitFlag = 16777216),
recursivelyAccumulateSuspenseyCommit(fiber),
(suspenseyCommitFlag = current))
: recursivelyAccumulateSuspenseyCommit(fiber);
}
break;
default:
recursivelyAccumulateSuspenseyCommit(fiber);
}
Expand Down Expand Up @@ -6756,11 +6772,12 @@ function handleThrow(root, thrownValue) {
? (root = null === shellBoundary ? !0 : !1)
: ((root = suspenseHandlerStackCursor.current),
(root =
null !== root &&
(workInProgressRootRenderLanes & 125829120) ===
workInProgressRootRenderLanes
? root === shellBoundary
: !1)),
null === root ||
((workInProgressRootRenderLanes & 125829120) !==
workInProgressRootRenderLanes &&
0 === (workInProgressRootRenderLanes & 1073741824))
? !1
: root === shellBoundary)),
(workInProgressSuspendedReason =
root &&
0 === (workInProgressRootSkippedLanes & 268435455) &&
Expand Down Expand Up @@ -8618,19 +8635,19 @@ function wrapFiber(fiber) {
fiberToWrapper.set(fiber, wrapper));
return wrapper;
}
var devToolsConfig$jscomp$inline_1002 = {
var devToolsConfig$jscomp$inline_1007 = {
findFiberByHostInstance: function () {
throw Error("TestRenderer does not support findFiberByHostInstance()");
},
bundleType: 0,
version: "18.3.0-next-d12bdcda6-20230325",
version: "18.3.0-next-768f965de-20230326",
rendererPackageName: "react-test-renderer"
};
var internals$jscomp$inline_1193 = {
bundleType: devToolsConfig$jscomp$inline_1002.bundleType,
version: devToolsConfig$jscomp$inline_1002.version,
rendererPackageName: devToolsConfig$jscomp$inline_1002.rendererPackageName,
rendererConfig: devToolsConfig$jscomp$inline_1002.rendererConfig,
var internals$jscomp$inline_1198 = {
bundleType: devToolsConfig$jscomp$inline_1007.bundleType,
version: devToolsConfig$jscomp$inline_1007.version,
rendererPackageName: devToolsConfig$jscomp$inline_1007.rendererPackageName,
rendererConfig: devToolsConfig$jscomp$inline_1007.rendererConfig,
overrideHookState: null,
overrideHookStateDeletePath: null,
overrideHookStateRenamePath: null,
Expand All @@ -8647,26 +8664,26 @@ var internals$jscomp$inline_1193 = {
return null === fiber ? null : fiber.stateNode;
},
findFiberByHostInstance:
devToolsConfig$jscomp$inline_1002.findFiberByHostInstance ||
devToolsConfig$jscomp$inline_1007.findFiberByHostInstance ||
emptyFindFiberByHostInstance,
findHostInstancesForRefresh: null,
scheduleRefresh: null,
scheduleRoot: null,
setRefreshHandler: null,
getCurrentFiber: null,
reconcilerVersion: "18.3.0-next-d12bdcda6-20230325"
reconcilerVersion: "18.3.0-next-768f965de-20230326"
};
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
var hook$jscomp$inline_1194 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
var hook$jscomp$inline_1199 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
if (
!hook$jscomp$inline_1194.isDisabled &&
hook$jscomp$inline_1194.supportsFiber
!hook$jscomp$inline_1199.isDisabled &&
hook$jscomp$inline_1199.supportsFiber
)
try {
(rendererID = hook$jscomp$inline_1194.inject(
internals$jscomp$inline_1193
(rendererID = hook$jscomp$inline_1199.inject(
internals$jscomp$inline_1198
)),
(injectedHook = hook$jscomp$inline_1194);
(injectedHook = hook$jscomp$inline_1199);
} catch (err) {}
}
exports._Scheduler = Scheduler;
Expand Down
Loading

0 comments on commit 7f1afb7

Please sign in to comment.