Skip to content

Commit c596d33

Browse files
committed
Check if suspensey instance resolves in immediate task (#26427)
When rendering a suspensey resource that we haven't seen before, it may have loaded in the background while we were rendering. We should yield to the main thread to see if the load event fires in an immediate task. For example, if the resource for a link element has already loaded, its load event will fire in a task right after React yields to the main thread. Because the continuation task is not scheduled until right before React yields, the load event will ping React before it resumes. If this happens, we can resume rendering without showing a fallback. I don't think this matters much for images, because the `completed` property tells us whether the image has loaded, and during a non-urgent render, we never block the main thread for more than 5ms at a time (for now — we might increase this in the future). It matters more for stylesheets because the only way to check if it has loaded is by listening for the load event. This is essentially the same trick that `use` does for userspace promises, but a bit simpler because we don't need to replay the host component's begin phase; the work-in-progress fiber already completed, so we can just continue onto the next sibling without any additional work. As part of this change, I split the `shouldSuspendCommit` host config method into separate `maySuspendCommit` and `preloadInstance` methods. Previously `shouldSuspendCommit` was used for both. This raised a question of whether we should preload resources during a synchronous render. My initial instinct was that we shouldn't, because we're going to synchronously block the main thread until the resource is inserted into the DOM, anyway. But I wonder if the browser is able to initiate the preload even while the main thread is blocked. It's probably a micro-optimization either way because most resources will be loaded during transitions, not urgent renders. DiffTrain build for [0131d0c](0131d0c)
1 parent e49c66b commit c596d33

19 files changed

+2147
-1144
lines changed

compiled/facebook-www/REVISION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
db281b3d9cd033cdc3d63e00fc9f3153c03aa70c
1+
0131d0cff40d4054ac72c857d3a13c5173c46e0a

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-eb67b0a3";
30+
var ReactVersion = "18.3.0-www-modern-1ad200fa";
3131

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

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

Lines changed: 100 additions & 11 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-5da08f8d";
72+
var ReactVersion = "18.3.0-www-classic-ca4ffb9c";
7373

7474
var LegacyRoot = 0;
7575
var ConcurrentRoot = 1;
@@ -2921,6 +2921,10 @@ function unhideTextInstance(textInstance, text) {
29212921
function getInstanceFromNode(node) {
29222922
throw new Error("Not implemented.");
29232923
}
2924+
function preloadInstance(type, props) {
2925+
// Return true to indicate it's already loaded
2926+
return true;
2927+
}
29242928
function waitForCommitToBeReady() {
29252929
return null;
29262930
} // eslint-disable-next-line no-undef
@@ -5453,6 +5457,10 @@ var SuspenseException = new Error(
54535457
"unexpected behavior.\n\n" +
54545458
"To handle async errors, wrap your component in an error boundary, or " +
54555459
"call the promise's `.catch` method and pass the result to `use`"
5460+
);
5461+
var SuspenseyCommitException = new Error(
5462+
"Suspense Exception: This is not a real error, and should not leak into " +
5463+
"userspace. If you're seeing this, it's likely a bug in React."
54565464
); // This is a noop thenable that we use to trigger a fallback in throwException.
54575465
// TODO: It would be better to refactor throwException into multiple functions
54585466
// so we can trigger a fallback directly without having to check the type. But
@@ -17636,7 +17644,6 @@ function updateHostComponent(
1763617644
// we won't touch this node even if children changed.
1763717645
return;
1763817646
} // If we get updated because one of our children updated, we don't
17639-
suspendHostCommitIfNeeded(workInProgress);
1764017647
getHostContext(); // TODO: Experiencing an error where oldProps is null. Suggests a host
1764117648
// component is hitting the resume path. Figure out why. Possibly
1764217649
// related to `hidden`.
@@ -17655,12 +17662,17 @@ function updateHostComponent(
1765517662
// that suspend don't have children, so it doesn't matter. But that might not
1765617663
// always be true in the future.
1765717664

17658-
function suspendHostCommitIfNeeded(workInProgress, type, props, renderLanes) {
17665+
function preloadInstanceAndSuspendIfNeeded(
17666+
workInProgress,
17667+
type,
17668+
props,
17669+
renderLanes
17670+
) {
1765917671
// Ask the renderer if this instance should suspend the commit.
1766017672
{
1766117673
// If this flag was set previously, we can remove it. The flag represents
1766217674
// whether this particular set of props might ever need to suspend. The
17663-
// safest thing to do is for shouldSuspendCommit to always return true, but
17675+
// safest thing to do is for maySuspendCommit to always return true, but
1766417676
// if the renderer is reasonably confident that the underlying resource
1766517677
// won't be evicted, it can return false as a performance optimization.
1766617678
workInProgress.flags &= ~SuspenseyCommit;
@@ -18141,15 +18153,18 @@ function completeWork(current, workInProgress, renderLanes) {
1814118153
workInProgress.stateNode = _instance3; // Certain renderers require commit-time effects for initial mount.
1814218154
}
1814318155

18144-
suspendHostCommitIfNeeded(workInProgress);
18145-
1814618156
if (workInProgress.ref !== null) {
1814718157
// If there is a ref on a host node we need to schedule a callback
1814818158
markRef(workInProgress);
1814918159
}
1815018160
}
1815118161

18152-
bubbleProperties(workInProgress);
18162+
bubbleProperties(workInProgress); // This must come at the very end of the complete phase, because it might
18163+
// throw to suspend, and if the resource immediately loads, the work loop
18164+
// will resume rendering as if the work-in-progress completed. So it must
18165+
// fully complete.
18166+
18167+
preloadInstanceAndSuspendIfNeeded(workInProgress);
1815318168
return null;
1815418169
}
1815518170

@@ -23119,9 +23134,11 @@ var NotSuspended = 0;
2311923134
var SuspendedOnError = 1;
2312023135
var SuspendedOnData = 2;
2312123136
var SuspendedOnImmediate = 3;
23122-
var SuspendedOnDeprecatedThrowPromise = 4;
23123-
var SuspendedAndReadyToContinue = 5;
23124-
var SuspendedOnHydration = 6; // When this is true, the work-in-progress fiber just suspended (or errored) and
23137+
var SuspendedOnInstance = 4;
23138+
var SuspendedOnInstanceAndReadyToContinue = 5;
23139+
var SuspendedOnDeprecatedThrowPromise = 6;
23140+
var SuspendedAndReadyToContinue = 7;
23141+
var SuspendedOnHydration = 8; // When this is true, the work-in-progress fiber just suspended (or errored) and
2312523142
// we've yet to unwind the stack. In some cases, we may yield to the main thread
2312623143
// after this happens. If the fiber is pinged before we resume, we can retry
2312723144
// immediately instead of unwinding the stack.
@@ -24477,6 +24494,9 @@ function handleThrow(root, thrownValue) {
2447724494
: // immediately resolved (i.e. in a microtask). Otherwise, trigger the
2447824495
// nearest Suspense fallback.
2447924496
SuspendedOnImmediate;
24497+
} else if (thrownValue === SuspenseyCommitException) {
24498+
thrownValue = getSuspendedThenable();
24499+
workInProgressSuspendedReason = SuspendedOnInstance;
2448024500
} else if (thrownValue === SelectiveHydrationException) {
2448124501
// An update flowed into a dehydrated boundary. Before we can apply the
2448224502
// update, we need to finish hydrating. Interrupt the work-in-progress
@@ -24847,7 +24867,7 @@ function renderRootConcurrent(root, lanes) {
2484724867
var unitOfWork = workInProgress;
2484824868
var thrownValue = workInProgressThrownValue;
2484924869

24850-
switch (workInProgressSuspendedReason) {
24870+
resumeOrUnwind: switch (workInProgressSuspendedReason) {
2485124871
case SuspendedOnError: {
2485224872
// Unwind then continue with the normal work loop.
2485324873
workInProgressSuspendedReason = NotSuspended;
@@ -24899,6 +24919,12 @@ function renderRootConcurrent(root, lanes) {
2489924919
break outer;
2490024920
}
2490124921

24922+
case SuspendedOnInstance: {
24923+
workInProgressSuspendedReason =
24924+
SuspendedOnInstanceAndReadyToContinue;
24925+
break outer;
24926+
}
24927+
2490224928
case SuspendedAndReadyToContinue: {
2490324929
var _thenable = thrownValue;
2490424930

@@ -24917,6 +24943,69 @@ function renderRootConcurrent(root, lanes) {
2491724943
break;
2491824944
}
2491924945

24946+
case SuspendedOnInstanceAndReadyToContinue: {
24947+
switch (workInProgress.tag) {
24948+
case HostComponent:
24949+
case HostHoistable:
24950+
case HostSingleton: {
24951+
// Before unwinding the stack, check one more time if the
24952+
// instance is ready. It may have loaded when React yielded to
24953+
// the main thread.
24954+
// Assigning this to a constant so Flow knows the binding won't
24955+
// be mutated by `preloadInstance`.
24956+
var hostFiber = workInProgress;
24957+
var type = hostFiber.type;
24958+
var props = hostFiber.pendingProps;
24959+
var isReady = preloadInstance(type, props);
24960+
24961+
if (isReady) {
24962+
// The data resolved. Resume the work loop as if nothing
24963+
// suspended. Unlike when a user component suspends, we don't
24964+
// have to replay anything because the host fiber
24965+
// already completed.
24966+
workInProgressSuspendedReason = NotSuspended;
24967+
workInProgressThrownValue = null;
24968+
var sibling = hostFiber.sibling;
24969+
24970+
if (sibling !== null) {
24971+
workInProgress = sibling;
24972+
} else {
24973+
var returnFiber = hostFiber.return;
24974+
24975+
if (returnFiber !== null) {
24976+
workInProgress = returnFiber;
24977+
completeUnitOfWork(returnFiber);
24978+
} else {
24979+
workInProgress = null;
24980+
}
24981+
}
24982+
24983+
break resumeOrUnwind;
24984+
}
24985+
24986+
break;
24987+
}
24988+
24989+
default: {
24990+
// This will fail gracefully but it's not correct, so log a
24991+
// warning in dev.
24992+
if (true) {
24993+
error(
24994+
"Unexpected type of fiber triggered a suspensey commit. " +
24995+
"This is a bug in React."
24996+
);
24997+
}
24998+
24999+
break;
25000+
}
25001+
} // Otherwise, unwind then continue with the normal work loop.
25002+
25003+
workInProgressSuspendedReason = NotSuspended;
25004+
workInProgressThrownValue = null;
25005+
unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
25006+
break;
25007+
}
25008+
2492025009
case SuspendedOnDeprecatedThrowPromise: {
2492125010
// Suspended by an old implementation that uses the `throw promise`
2492225011
// pattern. The newer replaying behavior can cause subtle issues

0 commit comments

Comments
 (0)