[Fizz] Add "Queued" Status to SSR:ed Suspense Boundaries (#33087)

Stacked on #33076.

This fixes a bug where we used the "complete" status but the
DOMContentLoaded event. This checks for not "loading" instead.

We also add a new status where the boundary has been marked as complete
by the server but has not yet flushed either due to being throttled,
suspended on CSS or animating.
This commit is contained in:
Sebastian Markbåge
2025-05-01 16:11:54 -04:00
committed by GitHub
parent ee7fee8f88
commit 0ed6ceb9f6
3 changed files with 42 additions and 15 deletions
+26 -13
View File
@@ -238,6 +238,7 @@ const ACTIVITY_END_DATA = '/&';
const SUSPENSE_START_DATA = '$';
const SUSPENSE_END_DATA = '/$';
const SUSPENSE_PENDING_START_DATA = '$?';
const SUSPENSE_QUEUED_START_DATA = '$~';
const SUSPENSE_FALLBACK_START_DATA = '$!';
const PREAMBLE_CONTRIBUTION_HTML = 'html';
const PREAMBLE_CONTRIBUTION_BODY = 'body';
@@ -245,7 +246,7 @@ const PREAMBLE_CONTRIBUTION_HEAD = 'head';
const FORM_STATE_IS_MATCHING = 'F!';
const FORM_STATE_IS_NOT_MATCHING = 'F';
const DOCUMENT_READY_STATE_COMPLETE = 'complete';
const DOCUMENT_READY_STATE_LOADING = 'loading';
const STYLE = 'style';
@@ -1084,6 +1085,7 @@ function clearHydrationBoundary(
} else if (
data === SUSPENSE_START_DATA ||
data === SUSPENSE_PENDING_START_DATA ||
data === SUSPENSE_QUEUED_START_DATA ||
data === SUSPENSE_FALLBACK_START_DATA ||
data === ACTIVITY_START_DATA
) {
@@ -1205,6 +1207,7 @@ function hideOrUnhideDehydratedBoundary(
} else if (
data === SUSPENSE_START_DATA ||
data === SUSPENSE_PENDING_START_DATA ||
data === SUSPENSE_QUEUED_START_DATA ||
data === SUSPENSE_FALLBACK_START_DATA
) {
depth++;
@@ -3140,7 +3143,10 @@ export function canHydrateSuspenseInstance(
}
export function isSuspenseInstancePending(instance: SuspenseInstance): boolean {
return instance.data === SUSPENSE_PENDING_START_DATA;
return (
instance.data === SUSPENSE_PENDING_START_DATA ||
instance.data === SUSPENSE_QUEUED_START_DATA
);
}
export function isSuspenseInstanceFallback(
@@ -3149,7 +3155,7 @@ export function isSuspenseInstanceFallback(
return (
instance.data === SUSPENSE_FALLBACK_START_DATA ||
(instance.data === SUSPENSE_PENDING_START_DATA &&
instance.ownerDocument.readyState === DOCUMENT_READY_STATE_COMPLETE)
instance.ownerDocument.readyState !== DOCUMENT_READY_STATE_LOADING)
);
}
@@ -3192,7 +3198,11 @@ export function registerSuspenseInstanceRetry(
callback: () => void,
) {
const ownerDocument = instance.ownerDocument;
if (
if (instance.data === SUSPENSE_QUEUED_START_DATA) {
// The Fizz runtime has already queued this boundary for reveal. We wait for it
// to be revealed and then retries.
instance._reactRetry = callback;
} else if (
// The Fizz runtime must have put this boundary into client render or complete
// state after the render finished but before it committed. We need to call the
// callback now rather than wait
@@ -3200,7 +3210,7 @@ export function registerSuspenseInstanceRetry(
// The boundary is still in pending status but the document has finished loading
// before we could register the event handler that would have scheduled the retry
// on load so we call teh callback now.
ownerDocument.readyState === DOCUMENT_READY_STATE_COMPLETE
ownerDocument.readyState !== DOCUMENT_READY_STATE_LOADING
) {
callback();
} else {
@@ -3255,18 +3265,19 @@ function getNextHydratable(node: ?Node) {
break;
}
if (nodeType === COMMENT_NODE) {
const nodeData = (node: any).data;
const data = (node: any).data;
if (
nodeData === SUSPENSE_START_DATA ||
nodeData === SUSPENSE_FALLBACK_START_DATA ||
nodeData === SUSPENSE_PENDING_START_DATA ||
nodeData === ACTIVITY_START_DATA ||
nodeData === FORM_STATE_IS_MATCHING ||
nodeData === FORM_STATE_IS_NOT_MATCHING
data === SUSPENSE_START_DATA ||
data === SUSPENSE_FALLBACK_START_DATA ||
data === SUSPENSE_PENDING_START_DATA ||
data === SUSPENSE_QUEUED_START_DATA ||
data === ACTIVITY_START_DATA ||
data === FORM_STATE_IS_MATCHING ||
data === FORM_STATE_IS_NOT_MATCHING
) {
break;
}
if (nodeData === SUSPENSE_END_DATA || nodeData === ACTIVITY_END_DATA) {
if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
return null;
}
}
@@ -3494,6 +3505,7 @@ function getNextHydratableInstanceAfterHydrationBoundary(
data === SUSPENSE_START_DATA ||
data === SUSPENSE_FALLBACK_START_DATA ||
data === SUSPENSE_PENDING_START_DATA ||
data === SUSPENSE_QUEUED_START_DATA ||
data === ACTIVITY_START_DATA
) {
depth++;
@@ -3535,6 +3547,7 @@ export function getParentHydrationBoundary(
data === SUSPENSE_START_DATA ||
data === SUSPENSE_FALLBACK_START_DATA ||
data === SUSPENSE_PENDING_START_DATA ||
data === SUSPENSE_QUEUED_START_DATA ||
data === ACTIVITY_START_DATA
) {
if (depth === 0) {
@@ -6,9 +6,9 @@ export const markShellTime =
export const clientRenderBoundary =
'$RX=function(b,c,d,e,f){var a=document.getElementById(b);a&&(b=a.previousSibling,b.data="$!",a=a.dataset,c&&(a.dgst=c),d&&(a.msg=d),e&&(a.stck=e),f&&(a.cstck=f),b._reactRetry&&b._reactRetry())};';
export const completeBoundary =
'$RB=[];$RC=function(e,c){function m(){$RT=performance.now();var f=$RB;$RB=[];for(var d=0;d<f.length;d+=2){var a=f[d],l=f[d+1],g=a.parentNode;if(g){var h=a.previousSibling,k=0;do{if(a&&8===a.nodeType){var b=a.data;if("/$"===b||"/&"===b)if(0===k)break;else k--;else"$"!==b&&"$?"!==b&&"$!"!==b&&"&"!==b||k++}b=a.nextSibling;g.removeChild(a);a=b}while(a);for(;l.firstChild;)g.insertBefore(l.firstChild,a);h.data="$";h._reactRetry&&h._reactRetry()}}}if(c=document.getElementById(c))if(c.parentNode.removeChild(c),e=\ndocument.getElementById(e))$RB.push(e,c),2===$RB.length&&setTimeout(m,("number"!==typeof $RT?0:$RT)+300-performance.now())};';
'$RB=[];$RC=function(d,c){function m(){$RT=performance.now();var f=$RB;$RB=[];for(var e=0;e<f.length;e+=2){var a=f[e],l=f[e+1],g=a.parentNode;if(g){var h=a.previousSibling,k=0;do{if(a&&8===a.nodeType){var b=a.data;if("/$"===b||"/&"===b)if(0===k)break;else k--;else"$"!==b&&"$?"!==b&&"$~"!==b&&"$!"!==b&&"&"!==b||k++}b=a.nextSibling;g.removeChild(a);a=b}while(a);for(;l.firstChild;)g.insertBefore(l.firstChild,a);h.data="$";h._reactRetry&&h._reactRetry()}}}if(c=document.getElementById(c))if(c.parentNode.removeChild(c),d=\ndocument.getElementById(d))d.previousSibling.data="$~",$RB.push(d,c),2===$RB.length&&setTimeout(m,("number"!==typeof $RT?0:$RT)+300-performance.now())};';
export const completeBoundaryWithStyles =
'$RM=new Map;$RR=function(r,v,w){function t(n){this._p=null;n()}for(var p=new Map,q=document,g,b,h=q.querySelectorAll("link[data-precedence],style[data-precedence]"),u=[],k=0;b=h[k++];)"not all"===b.getAttribute("media")?u.push(b):("LINK"===b.tagName&&$RM.set(b.getAttribute("href"),b),p.set(b.dataset.precedence,g=b));b=0;h=[];var l,a;for(k=!0;;){if(k){var e=w[b++];if(!e){k=!1;b=0;continue}var c=!1,m=0;var d=e[m++];if(a=$RM.get(d)){var f=a._p;c=!0}else{a=q.createElement("link");a.href=d;a.rel=\n"stylesheet";for(a.dataset.precedence=l=e[m++];f=e[m++];)a.setAttribute(f,e[m++]);f=a._p=new Promise(function(n,x){a.onload=t.bind(a,n);a.onerror=t.bind(a,x)});$RM.set(d,a)}d=a.getAttribute("media");!f||d&&!matchMedia(d).matches||h.push(f);if(c)continue}else{a=u[b++];if(!a)break;l=a.getAttribute("data-precedence");a.removeAttribute("media")}c=p.get(l)||g;c===g&&(g=a);p.set(l,a);c?c.parentNode.insertBefore(a,c.nextSibling):(c=q.head,c.insertBefore(a,c.firstChild))}Promise.all(h).then($RC.bind(null,\nr,v),$RX.bind(null,r,"CSS failed to load"))};';
'$RM=new Map;$RR=function(n,w,p){function u(q){this._p=null;q()}for(var r=new Map,t=document,h,b,e=t.querySelectorAll("link[data-precedence],style[data-precedence]"),v=[],k=0;b=e[k++];)"not all"===b.getAttribute("media")?v.push(b):("LINK"===b.tagName&&$RM.set(b.getAttribute("href"),b),r.set(b.dataset.precedence,h=b));e=0;b=[];var l,a;for(k=!0;;){if(k){var f=p[e++];if(!f){k=!1;e=0;continue}var c=!1,m=0;var d=f[m++];if(a=$RM.get(d)){var g=a._p;c=!0}else{a=t.createElement("link");a.href=d;a.rel=\n"stylesheet";for(a.dataset.precedence=l=f[m++];g=f[m++];)a.setAttribute(g,f[m++]);g=a._p=new Promise(function(q,x){a.onload=u.bind(a,q);a.onerror=u.bind(a,x)});$RM.set(d,a)}d=a.getAttribute("media");!g||d&&!matchMedia(d).matches||b.push(g);if(c)continue}else{a=v[e++];if(!a)break;l=a.getAttribute("data-precedence");a.removeAttribute("media")}c=r.get(l)||h;c===h&&(h=a);r.set(l,a);c?c.parentNode.insertBefore(a,c.nextSibling):(c=t.head,c.insertBefore(a,c.firstChild))}if(p=document.getElementById(n))p.previousSibling.data=\n"$~";Promise.all(b).then($RC.bind(null,n,w),$RX.bind(null,n,"CSS failed to load"))};';
export const completeSegment =
'$RS=function(a,b){a=document.getElementById(a);b=document.getElementById(b);for(a.parentNode.removeChild(a);a.firstChild;)b.parentNode.insertBefore(a.firstChild,b);b.parentNode.removeChild(b)};';
export const formReplaying =
@@ -9,6 +9,7 @@ const ACTIVITY_END_DATA = '/&';
const SUSPENSE_START_DATA = '$';
const SUSPENSE_END_DATA = '/$';
const SUSPENSE_PENDING_START_DATA = '$?';
const SUSPENSE_QUEUED_START_DATA = '$~';
const SUSPENSE_FALLBACK_START_DATA = '$!';
// TODO: Symbols that are referenced outside this module use dynamic accessor
@@ -106,6 +107,7 @@ export function completeBoundary(suspenseBoundaryID, contentID) {
} else if (
data === SUSPENSE_START_DATA ||
data === SUSPENSE_PENDING_START_DATA ||
data === SUSPENSE_QUEUED_START_DATA ||
data === SUSPENSE_FALLBACK_START_DATA ||
data === ACTIVITY_START_DATA
) {
@@ -132,6 +134,10 @@ export function completeBoundary(suspenseBoundaryID, contentID) {
}
}
// Mark this Suspense boundary as queued so we know not to client render it
// at the end of document load.
const suspenseNodeOuter = suspenseIdNodeOuter.previousSibling;
suspenseNodeOuter.data = SUSPENSE_QUEUED_START_DATA;
// Queue this boundary for the next batch
window['$RB'].push(suspenseIdNodeOuter, contentNodeOuter);
@@ -261,6 +267,14 @@ export function completeBoundaryWithStyles(
}
}
const suspenseIdNodeOuter = document.getElementById(suspenseBoundaryID);
if (suspenseIdNodeOuter) {
// Mark this Suspense boundary as queued so we know not to client render it
// at the end of document load.
const suspenseNodeOuter = suspenseIdNodeOuter.previousSibling;
suspenseNodeOuter.data = SUSPENSE_QUEUED_START_DATA;
}
Promise.all(dependencies).then(
window['$RC'].bind(null, suspenseBoundaryID, contentID),
window['$RX'].bind(null, suspenseBoundaryID, 'CSS failed to load'),