[Float] Suspend unstyled content for up to 1 minute (#26532)

We almost never want to show content before its styles have loaded. But
eventually we will give up and allow unstyled content. So this extends
the timeout to a full minute. This somewhat arbitrary — big enough that
you'd only reach it under extreme circumstances.

Note that, like regular Suspense, the app is still interactive while
we're waiting for content to load. Only the unstyled content is blocked
from appearing, not updates in general. A new update will interrupt it.

We should figure out what the browser engines do during initial page
load and consider aligning our behavior with that. It's supposed to be
render blocking by default but there may be some cases where they, too,
give up and FOUC.
This commit is contained in:
Andrew Clark 2023-03-31 15:45:45 -04:00 committed by GitHub
parent 888874673f
commit 0ae348018d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 38 additions and 17 deletions

View File

@ -3333,28 +3333,34 @@ export function waitForCommitToBeReady(): null | (Function => Function) {
// tasks to wait on.
if (state.count > 0) {
return commit => {
unsuspendAfterTimeout(state);
// We almost never want to show content before its styles have loaded. But
// eventually we will give up and allow unstyled content. So this number is
// somewhat arbitrary — big enough that you'd only reach it under
// extreme circumstances.
// TODO: Figure out what the browser engines do during initial page load and
// consider aligning our behavior with that.
const stylesheetTimer = setTimeout(() => {
if (state.stylesheets) {
insertSuspendedStylesheets(state, state.stylesheets);
}
if (state.unsuspend) {
const unsuspend = state.unsuspend;
state.unsuspend = null;
unsuspend();
}
}, 60000); // one minute
state.unsuspend = commit;
return () => (state.unsuspend = null);
return () => {
state.unsuspend = null;
clearTimeout(stylesheetTimer);
};
};
}
return null;
}
function unsuspendAfterTimeout(state: SuspendedState) {
setTimeout(() => {
if (state.stylesheets) {
insertSuspendedStylesheets(state, state.stylesheets);
}
if (state.unsuspend) {
const unsuspend = state.unsuspend;
state.unsuspend = null;
unsuspend();
}
}, 500);
}
function onUnsuspend(this: SuspendedState) {
this.count--;
if (this.count === 0) {

View File

@ -3163,7 +3163,7 @@ body {
);
});
it('can unsuspend after a timeout even if some assets never load', async () => {
it('stylesheets block render, with a really long timeout', async () => {
function App({children}) {
return (
<html>
@ -3191,7 +3191,22 @@ body {
</html>,
);
jest.advanceTimersByTime(1000);
// Advance time by 50 seconds. Even still, the transition is suspended.
jest.advanceTimersByTime(50000);
await waitForAll([]);
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>
<link rel="preload" href="foo" as="style" />
</head>
<body />
</html>,
);
// Advance time by 10 seconds more. A full minute total has elapsed. At this
// point, something must have really gone wrong, so we time out and allow
// unstyled content to be displayed.
jest.advanceTimersByTime(10000);
expect(getMeaningfulChildren(document)).toEqual(
<html>
<head>