Skip to content

Commit a90016b

Browse files
committed
[Fizz] Emit link rel="expect" to block render before the shell has fully loaded (#33016)
The semantics of React is that anything outside of Suspense boundaries in a transition doesn't display until it has fully unsuspended. With SSR streaming the intention is to preserve that. We explicitly don't want to support the mode of document streaming normally supported by the browser where it can paint content as tags stream in since that leads to content popping in and thrashing in unpredictable ways. This should instead be modeled explictly by nested Suspense boundaries or something like SuspenseList. After the first shell any nested Suspense boundaries are only revealed, by script, once they're fully streamed in to the next boundary. So this is already the case there. However, for the initial shell we have been at the mercy of browser heuristics for how long it decides to stream before the first paint. Chromium now has [an API explicitly for this use case](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API/Using#stabilizing_page_state_to_make_cross-document_transitions_consistent) that lets us model the semantics that we want. This is always important but especially so with MPA View Transitions. After this a simple document looks like this: ```html <!DOCTYPE html> <html> <head> <link rel="expect" href="#«R»" blocking="render"/> </head> <body> <p>hello world</p> <script src="bootstrap.js" id="«R»" async=""></script> ... </body> </html> ``` The `rel="expect"` tag indicates that we want to wait to paint until we have streamed far enough to be able to paint the id `"«R»"` which indicates the shell. Ideally this `id` would be assigned to the root most HTML element in the body. However, this is tricky in our implementation because there can be multiple and we can render them out of order. So instead, we assign the id to the first bootstrap script if there is one since these are always added to the end of the shell. If there isn't a bootstrap script then we emit an empty `<template id="«R»"></template>` instead as a marker. Since we currently put as much as possible in the shell if it's loaded by the time we render, this can have some negative effects for very large documents. We should instead apply the heuristic where very large Suspense boundaries get outlined outside the shell even if they're immediately available. This means that even prerenders can end up with script tags. We only emit the `rel="expect"` if you're rendering a whole document. I.e. if you rendered either a `<html>` or `<head>` tag. If you're rendering a partial document, then we don't really know where the streaming parts are anyway and can't provide such guarantees. This does apply whether you're streaming or not because we still want to block rendering until the end, but in practice any serialized state that needs hydrate should still be embedded after the completion id. DiffTrain build for [143d3e1](143d3e1)
1 parent 7c12c71 commit a90016b

36 files changed

+993
-651
lines changed

compiled/facebook-www/REVISION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
693803a9bb3073b2ff5c99f8ae804f855db9aae2
1+
143d3e1b89d7f64d607bbfc844d1324b39ed93dc
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
693803a9bb3073b2ff5c99f8ae804f855db9aae2
1+
143d3e1b89d7f64d607bbfc844d1324b39ed93dc

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1538,7 +1538,7 @@ __DEV__ &&
15381538
exports.useTransition = function () {
15391539
return resolveDispatcher().useTransition();
15401540
};
1541-
exports.version = "19.2.0-www-classic-693803a9-20250424";
1541+
exports.version = "19.2.0-www-classic-143d3e1b-20250425";
15421542
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
15431543
"function" ===
15441544
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1538,7 +1538,7 @@ __DEV__ &&
15381538
exports.useTransition = function () {
15391539
return resolveDispatcher().useTransition();
15401540
};
1541-
exports.version = "19.2.0-www-modern-693803a9-20250424";
1541+
exports.version = "19.2.0-www-modern-143d3e1b-20250425";
15421542
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
15431543
"function" ===
15441544
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/React-prod.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -636,4 +636,4 @@ exports.useSyncExternalStore = function (
636636
exports.useTransition = function () {
637637
return ReactSharedInternals.H.useTransition();
638638
};
639-
exports.version = "19.2.0-www-classic-693803a9-20250424";
639+
exports.version = "19.2.0-www-classic-143d3e1b-20250425";

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -636,4 +636,4 @@ exports.useSyncExternalStore = function (
636636
exports.useTransition = function () {
637637
return ReactSharedInternals.H.useTransition();
638638
};
639-
exports.version = "19.2.0-www-modern-693803a9-20250424";
639+
exports.version = "19.2.0-www-modern-143d3e1b-20250425";

compiled/facebook-www/React-profiling.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,7 @@ exports.useSyncExternalStore = function (
640640
exports.useTransition = function () {
641641
return ReactSharedInternals.H.useTransition();
642642
};
643-
exports.version = "19.2.0-www-classic-693803a9-20250424";
643+
exports.version = "19.2.0-www-classic-143d3e1b-20250425";
644644
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
645645
"function" ===
646646
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,7 @@ exports.useSyncExternalStore = function (
640640
exports.useTransition = function () {
641641
return ReactSharedInternals.H.useTransition();
642642
};
643-
exports.version = "19.2.0-www-modern-693803a9-20250424";
643+
exports.version = "19.2.0-www-modern-143d3e1b-20250425";
644644
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
645645
"function" ===
646646
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18800,10 +18800,10 @@ __DEV__ &&
1880018800
(function () {
1880118801
var internals = {
1880218802
bundleType: 1,
18803-
version: "19.2.0-www-classic-693803a9-20250424",
18803+
version: "19.2.0-www-classic-143d3e1b-20250425",
1880418804
rendererPackageName: "react-art",
1880518805
currentDispatcherRef: ReactSharedInternals,
18806-
reconcilerVersion: "19.2.0-www-classic-693803a9-20250424"
18806+
reconcilerVersion: "19.2.0-www-classic-143d3e1b-20250425"
1880718807
};
1880818808
internals.overrideHookState = overrideHookState;
1880918809
internals.overrideHookStateDeletePath = overrideHookStateDeletePath;
@@ -18837,7 +18837,7 @@ __DEV__ &&
1883718837
exports.Shape = Shape;
1883818838
exports.Surface = Surface;
1883918839
exports.Text = Text;
18840-
exports.version = "19.2.0-www-classic-693803a9-20250424";
18840+
exports.version = "19.2.0-www-classic-143d3e1b-20250425";
1884118841
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
1884218842
"function" ===
1884318843
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18572,10 +18572,10 @@ __DEV__ &&
1857218572
(function () {
1857318573
var internals = {
1857418574
bundleType: 1,
18575-
version: "19.2.0-www-modern-693803a9-20250424",
18575+
version: "19.2.0-www-modern-143d3e1b-20250425",
1857618576
rendererPackageName: "react-art",
1857718577
currentDispatcherRef: ReactSharedInternals,
18578-
reconcilerVersion: "19.2.0-www-modern-693803a9-20250424"
18578+
reconcilerVersion: "19.2.0-www-modern-143d3e1b-20250425"
1857918579
};
1858018580
internals.overrideHookState = overrideHookState;
1858118581
internals.overrideHookStateDeletePath = overrideHookStateDeletePath;
@@ -18609,7 +18609,7 @@ __DEV__ &&
1860918609
exports.Shape = Shape;
1861018610
exports.Surface = Surface;
1861118611
exports.Text = Text;
18612-
exports.version = "19.2.0-www-modern-693803a9-20250424";
18612+
exports.version = "19.2.0-www-modern-143d3e1b-20250425";
1861318613
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
1861418614
"function" ===
1861518615
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

0 commit comments

Comments
 (0)