Skip to content

Commit 7f1afb7

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 commit 768f965.
1 parent e591dc4 commit 7f1afb7

File tree

13 files changed

+464
-305
lines changed

13 files changed

+464
-305
lines changed

compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react-test-renderer/cjs/ReactTestRenderer-dev.js

Lines changed: 80 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -484,11 +484,12 @@ var Visibility =
484484
8192;
485485
var StoreConsistency =
486486
/* */
487-
16384; // It's OK to reuse this bit because these flags are mutually exclusive for
487+
16384; // It's OK to reuse these bits because these flags are mutually exclusive for
488488
// different fiber types. We should really be doing this for as many flags as
489489
// possible, because we're about to run out of bits.
490490

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

@@ -522,8 +523,8 @@ var LayoutStatic =
522523
var PassiveStatic =
523524
/* */
524525
8388608;
525-
var SuspenseyCommit =
526-
/* */
526+
var MaySuspendCommit =
527+
/* */
527528
16777216; // Flag used to identify newly inserted fibers. It isn't reset after commit unlike `Placement`.
528529

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

557-
var StaticMask = LayoutStatic | PassiveStatic | RefStatic | SuspenseyCommit;
558+
var StaticMask = LayoutStatic | PassiveStatic | RefStatic | MaySuspendCommit;
558559

559560
var ReactCurrentOwner$2 = ReactSharedInternals.ReactCurrentOwner;
560561
function getNearestMountedFiber(fiber) {
@@ -2023,9 +2024,6 @@ function unhideInstance(instance, props) {
20232024
function unhideTextInstance(textInstance, text) {
20242025
textInstance.isHidden = false;
20252026
}
2026-
function maySuspendCommit(type, props) {
2027-
return false;
2028-
}
20292027
function preloadInstance(type, props) {
20302028
// Return true to indicate it's already loaded
20312029
return true;
@@ -4310,13 +4308,6 @@ function trackUsedThenable(thenableState, thenable, index) {
43104308
}
43114309
}
43124310
}
4313-
function suspendCommit() {
4314-
// This extra indirection only exists so it can handle passing
4315-
// noopSuspenseyCommitThenable through to throwException.
4316-
// TODO: Factor the thenable check out of throwException
4317-
suspendedThenable = noopSuspenseyCommitThenable;
4318-
throw SuspenseyCommitException;
4319-
} // This is used to track the actual thenable that suspended so it can be
43204311
// passed to the rest of the Suspense implementation — which, for historical
43214312
// reasons, expects to receive a thenable.
43224313

@@ -14604,7 +14595,11 @@ function updateHostComponent(
1460414595
markUpdate(workInProgress);
1460514596
}
1460614597
}
14607-
} // TODO: This should ideally move to begin phase, but currently the instance is
14598+
} // This function must be called at the very end of the complete phase, because
14599+
// it might throw to suspend, and if the resource immediately loads, the work
14600+
// loop will resume rendering as if the work-in-progress completed. So it must
14601+
// fully complete.
14602+
// TODO: This should ideally move to begin phase, but currently the instance is
1460814603
// not created until the complete phase. For our existing use cases, host nodes
1460914604
// that suspend don't have children, so it doesn't matter. But that might not
1461014605
// always be true in the future.
@@ -14615,28 +14610,16 @@ function preloadInstanceAndSuspendIfNeeded(
1461514610
props,
1461614611
renderLanes
1461714612
) {
14618-
workInProgress.flags |= SuspenseyCommit; // Check if we're rendering at a "non-urgent" priority. This is the same
14619-
// check that `useDeferredValue` does to determine whether it needs to
14620-
// defer. This is partly for gradual adoption purposes (i.e. shouldn't start
14621-
// suspending until you opt in with startTransition or Suspense) but it
14622-
// also happens to be the desired behavior for the concrete use cases we've
14623-
// thought of so far, like CSS loading, fonts, images, etc.
14624-
// TODO: We may decide to expose a way to force a fallback even during a
14625-
// sync update.
14626-
14627-
if (!includesOnlyNonUrgentLanes(renderLanes));
14628-
else {
14629-
// Preload the instance
14630-
var isReady = preloadInstance();
14631-
14632-
if (!isReady) {
14633-
if (shouldRemainOnPreviousScreen());
14634-
else {
14635-
// Trigger a fallback rather than block the render.
14636-
suspendCommit();
14637-
}
14638-
}
14639-
}
14613+
{
14614+
// If this flag was set previously, we can remove it. The flag
14615+
// represents whether this particular set of props might ever need to
14616+
// suspend. The safest thing to do is for maySuspendCommit to always
14617+
// return true, but if the renderer is reasonably confident that the
14618+
// underlying resource won't be evicted, it can return false as a
14619+
// performance optimization.
14620+
workInProgress.flags &= ~MaySuspendCommit;
14621+
return;
14622+
} // Mark this fiber with a flag. This gets set on all host instances
1464014623
}
1464114624

1464214625
function scheduleRetryEffect(workInProgress, retryQueue) {
@@ -15042,12 +15025,10 @@ function completeWork(current, workInProgress, renderLanes) {
1504215025

1504315026
case HostComponent: {
1504415027
popHostContext(workInProgress);
15045-
var _type = workInProgress.type;
15046-
15047-
var _maySuspend = maySuspendCommit();
15028+
var _type2 = workInProgress.type;
1504815029

1504915030
if (current !== null && workInProgress.stateNode != null) {
15050-
updateHostComponent(current, workInProgress, _type, newProps);
15031+
updateHostComponent(current, workInProgress, _type2, newProps);
1505115032

1505215033
if (current.ref !== workInProgress.ref) {
1505315034
markRef(workInProgress);
@@ -15084,7 +15065,7 @@ function completeWork(current, workInProgress, renderLanes) {
1508415065
var _rootContainerInstance = getRootHostContainer();
1508515066

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

15109-
if (_maySuspend) {
15110-
preloadInstanceAndSuspendIfNeeded(
15111-
workInProgress,
15112-
_type,
15113-
newProps,
15114-
renderLanes
15115-
);
15116-
} else {
15117-
workInProgress.flags &= ~SuspenseyCommit;
15118-
}
15119-
15090+
preloadInstanceAndSuspendIfNeeded(workInProgress);
1512015091
return null;
1512115092
}
1512215093

@@ -18973,13 +18944,24 @@ function commitPassiveUnmountEffects(finishedWork) {
1897318944
setCurrentFiber(finishedWork);
1897418945
commitPassiveUnmountOnFiber(finishedWork);
1897518946
resetCurrentFiber();
18976-
}
18947+
} // If we're inside a brand new tree, or a tree that was already visible, then we
18948+
// should only suspend host components that have a ShouldSuspendCommit flag.
18949+
// Components without it haven't changed since the last commit, so we can skip
18950+
// over those.
18951+
//
18952+
// When we enter a tree that is being revealed (going from hidden -> visible),
18953+
// we need to suspend _any_ component that _may_ suspend. Even if they're
18954+
// already in the "current" tree. Because their visibility has changed, the
18955+
// browser may not have prerendered them yet. So we check the MaySuspendCommit
18956+
// flag instead.
18957+
18958+
var suspenseyCommitFlag = ShouldSuspendCommit;
1897718959
function accumulateSuspenseyCommit(finishedWork) {
1897818960
accumulateSuspenseyCommitOnFiber(finishedWork);
1897918961
}
1898018962

1898118963
function recursivelyAccumulateSuspenseyCommit(parentFiber) {
18982-
if (parentFiber.subtreeFlags & SuspenseyCommit) {
18964+
if (parentFiber.subtreeFlags & suspenseyCommitFlag) {
1898318965
var child = parentFiber.child;
1898418966

1898518967
while (child !== null) {
@@ -18994,7 +18976,7 @@ function accumulateSuspenseyCommitOnFiber(fiber) {
1899418976
case HostHoistable: {
1899518977
recursivelyAccumulateSuspenseyCommit(fiber);
1899618978

18997-
if (fiber.flags & SuspenseyCommit) {
18979+
if (fiber.flags & suspenseyCommitFlag) {
1899818980
if (fiber.memoizedState !== null) {
1899918981
suspendResource();
1900018982
}
@@ -19010,8 +18992,36 @@ function accumulateSuspenseyCommitOnFiber(fiber) {
1901018992
}
1901118993

1901218994
case HostRoot:
19013-
case HostPortal:
19014-
// eslint-disable-next-line-no-fallthrough
18995+
case HostPortal: {
18996+
{
18997+
recursivelyAccumulateSuspenseyCommit(fiber);
18998+
}
18999+
19000+
break;
19001+
}
19002+
19003+
case OffscreenComponent: {
19004+
var isHidden = fiber.memoizedState !== null;
19005+
19006+
if (isHidden);
19007+
else {
19008+
var current = fiber.alternate;
19009+
var wasHidden = current !== null && current.memoizedState !== null;
19010+
19011+
if (wasHidden) {
19012+
// This tree is being revealed. Visit all newly visible suspensey
19013+
// instances, even if they're in the current tree.
19014+
var prevFlags = suspenseyCommitFlag;
19015+
suspenseyCommitFlag = MaySuspendCommit;
19016+
recursivelyAccumulateSuspenseyCommit(fiber);
19017+
suspenseyCommitFlag = prevFlags;
19018+
} else {
19019+
recursivelyAccumulateSuspenseyCommit(fiber);
19020+
}
19021+
}
19022+
19023+
break;
19024+
}
1901519025

1901619026
default: {
1901719027
recursivelyAccumulateSuspenseyCommit(fiber);
@@ -20792,15 +20802,24 @@ function shouldRemainOnPreviousScreen() {
2079220802

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

2080520824
return false;
2080620825
}
@@ -23743,7 +23762,7 @@ function createFiberRoot(
2374323762
return root;
2374423763
}
2374523764

23746-
var ReactVersion = "18.3.0-next-d12bdcda6-20230325";
23765+
var ReactVersion = "18.3.0-next-768f965de-20230326";
2374723766

2374823767
// Might add PROFILE later.
2374923768

compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react-test-renderer/cjs/ReactTestRenderer-prod.js

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6038,8 +6038,9 @@ function recursivelyTraverseAtomicPassiveEffects(
60386038
parentFiber = parentFiber.sibling;
60396039
}
60406040
}
6041+
var suspenseyCommitFlag = 8192;
60416042
function recursivelyAccumulateSuspenseyCommit(parentFiber) {
6042-
if (parentFiber.subtreeFlags & 16777216)
6043+
if (parentFiber.subtreeFlags & suspenseyCommitFlag)
60436044
for (parentFiber = parentFiber.child; null !== parentFiber; )
60446045
accumulateSuspenseyCommitOnFiber(parentFiber),
60456046
(parentFiber = parentFiber.sibling);
@@ -6048,14 +6049,29 @@ function accumulateSuspenseyCommitOnFiber(fiber) {
60486049
switch (fiber.tag) {
60496050
case 26:
60506051
recursivelyAccumulateSuspenseyCommit(fiber);
6051-
if (fiber.flags & 16777216 && null !== fiber.memoizedState)
6052+
if (fiber.flags & suspenseyCommitFlag && null !== fiber.memoizedState)
60526053
throw Error(
60536054
"The current renderer does not support Resources. This error is likely caused by a bug in React. Please file an issue."
60546055
);
60556056
break;
60566057
case 5:
60576058
recursivelyAccumulateSuspenseyCommit(fiber);
60586059
break;
6060+
case 3:
6061+
case 4:
6062+
recursivelyAccumulateSuspenseyCommit(fiber);
6063+
break;
6064+
case 22:
6065+
if (null === fiber.memoizedState) {
6066+
var current = fiber.alternate;
6067+
null !== current && null !== current.memoizedState
6068+
? ((current = suspenseyCommitFlag),
6069+
(suspenseyCommitFlag = 16777216),
6070+
recursivelyAccumulateSuspenseyCommit(fiber),
6071+
(suspenseyCommitFlag = current))
6072+
: recursivelyAccumulateSuspenseyCommit(fiber);
6073+
}
6074+
break;
60596075
default:
60606076
recursivelyAccumulateSuspenseyCommit(fiber);
60616077
}
@@ -6756,11 +6772,12 @@ function handleThrow(root, thrownValue) {
67566772
? (root = null === shellBoundary ? !0 : !1)
67576773
: ((root = suspenseHandlerStackCursor.current),
67586774
(root =
6759-
null !== root &&
6760-
(workInProgressRootRenderLanes & 125829120) ===
6761-
workInProgressRootRenderLanes
6762-
? root === shellBoundary
6763-
: !1)),
6775+
null === root ||
6776+
((workInProgressRootRenderLanes & 125829120) !==
6777+
workInProgressRootRenderLanes &&
6778+
0 === (workInProgressRootRenderLanes & 1073741824))
6779+
? !1
6780+
: root === shellBoundary)),
67646781
(workInProgressSuspendedReason =
67656782
root &&
67666783
0 === (workInProgressRootSkippedLanes & 268435455) &&
@@ -8618,19 +8635,19 @@ function wrapFiber(fiber) {
86188635
fiberToWrapper.set(fiber, wrapper));
86198636
return wrapper;
86208637
}
8621-
var devToolsConfig$jscomp$inline_1002 = {
8638+
var devToolsConfig$jscomp$inline_1007 = {
86228639
findFiberByHostInstance: function () {
86238640
throw Error("TestRenderer does not support findFiberByHostInstance()");
86248641
},
86258642
bundleType: 0,
8626-
version: "18.3.0-next-d12bdcda6-20230325",
8643+
version: "18.3.0-next-768f965de-20230326",
86278644
rendererPackageName: "react-test-renderer"
86288645
};
8629-
var internals$jscomp$inline_1193 = {
8630-
bundleType: devToolsConfig$jscomp$inline_1002.bundleType,
8631-
version: devToolsConfig$jscomp$inline_1002.version,
8632-
rendererPackageName: devToolsConfig$jscomp$inline_1002.rendererPackageName,
8633-
rendererConfig: devToolsConfig$jscomp$inline_1002.rendererConfig,
8646+
var internals$jscomp$inline_1198 = {
8647+
bundleType: devToolsConfig$jscomp$inline_1007.bundleType,
8648+
version: devToolsConfig$jscomp$inline_1007.version,
8649+
rendererPackageName: devToolsConfig$jscomp$inline_1007.rendererPackageName,
8650+
rendererConfig: devToolsConfig$jscomp$inline_1007.rendererConfig,
86348651
overrideHookState: null,
86358652
overrideHookStateDeletePath: null,
86368653
overrideHookStateRenamePath: null,
@@ -8647,26 +8664,26 @@ var internals$jscomp$inline_1193 = {
86478664
return null === fiber ? null : fiber.stateNode;
86488665
},
86498666
findFiberByHostInstance:
8650-
devToolsConfig$jscomp$inline_1002.findFiberByHostInstance ||
8667+
devToolsConfig$jscomp$inline_1007.findFiberByHostInstance ||
86518668
emptyFindFiberByHostInstance,
86528669
findHostInstancesForRefresh: null,
86538670
scheduleRefresh: null,
86548671
scheduleRoot: null,
86558672
setRefreshHandler: null,
86568673
getCurrentFiber: null,
8657-
reconcilerVersion: "18.3.0-next-d12bdcda6-20230325"
8674+
reconcilerVersion: "18.3.0-next-768f965de-20230326"
86588675
};
86598676
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
8660-
var hook$jscomp$inline_1194 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
8677+
var hook$jscomp$inline_1199 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
86618678
if (
8662-
!hook$jscomp$inline_1194.isDisabled &&
8663-
hook$jscomp$inline_1194.supportsFiber
8679+
!hook$jscomp$inline_1199.isDisabled &&
8680+
hook$jscomp$inline_1199.supportsFiber
86648681
)
86658682
try {
8666-
(rendererID = hook$jscomp$inline_1194.inject(
8667-
internals$jscomp$inline_1193
8683+
(rendererID = hook$jscomp$inline_1199.inject(
8684+
internals$jscomp$inline_1198
86688685
)),
8669-
(injectedHook = hook$jscomp$inline_1194);
8686+
(injectedHook = hook$jscomp$inline_1199);
86708687
} catch (err) {}
86718688
}
86728689
exports._Scheduler = Scheduler;

0 commit comments

Comments
 (0)