@samitouri / QOS-React-1 / commits / 0ed6ceb9f6

[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.

Sebastian Markbåge committed May 1, 2025 at 16:11 UTC 0ed6ceb9f6c19f28c504cf46193cef40166a61f6
3 files changed +42 -15
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+26 -13
@@ -238,6 +238,7 @@ const ACTIVITY_END_DATA = '/&';
238 const SUSPENSE_START_DATA = '$';
239 const SUSPENSE_END_DATA = '/$';
240 const SUSPENSE_PENDING_START_DATA = '$?';
241 +const SUSPENSE_QUEUED_START_DATA = '$~';
242 const SUSPENSE_FALLBACK_START_DATA = '$!';
243 const PREAMBLE_CONTRIBUTION_HTML = 'html';
244 const PREAMBLE_CONTRIBUTION_BODY = 'body';
@@ -245,7 +246,7 @@ const PREAMBLE_CONTRIBUTION_HEAD = 'head';
246 const FORM_STATE_IS_MATCHING = 'F!';
247 const FORM_STATE_IS_NOT_MATCHING = 'F';
248
248 -const DOCUMENT_READY_STATE_COMPLETE = 'complete';
249 +const DOCUMENT_READY_STATE_LOADING = 'loading';
250
251 const STYLE = 'style';
252
@@ -1084,6 +1085,7 @@ function clearHydrationBoundary(
1085 } else if (
1086 data === SUSPENSE_START_DATA ||
1087 data === SUSPENSE_PENDING_START_DATA ||
1088 + data === SUSPENSE_QUEUED_START_DATA ||
1089 data === SUSPENSE_FALLBACK_START_DATA ||
1090 data === ACTIVITY_START_DATA
1091 ) {
@@ -1205,6 +1207,7 @@ function hideOrUnhideDehydratedBoundary(
1207 } else if (
1208 data === SUSPENSE_START_DATA ||
1209 data === SUSPENSE_PENDING_START_DATA ||
1210 + data === SUSPENSE_QUEUED_START_DATA ||
1211 data === SUSPENSE_FALLBACK_START_DATA
1212 ) {
1213 depth++;
@@ -3140,7 +3143,10 @@ export function canHydrateSuspenseInstance(
3143 }
3144
3145 export function isSuspenseInstancePending(instance: SuspenseInstance): boolean {
3143 - return instance.data === SUSPENSE_PENDING_START_DATA;
3146 + return (
3147 + instance.data === SUSPENSE_PENDING_START_DATA ||
3148 + instance.data === SUSPENSE_QUEUED_START_DATA
3149 + );
3150 }
3151
3152 export function isSuspenseInstanceFallback(
@@ -3149,7 +3155,7 @@ export function isSuspenseInstanceFallback(
3155 return (
3156 instance.data === SUSPENSE_FALLBACK_START_DATA ||
3157 (instance.data === SUSPENSE_PENDING_START_DATA &&
3152 - instance.ownerDocument.readyState === DOCUMENT_READY_STATE_COMPLETE)
3158 + instance.ownerDocument.readyState !== DOCUMENT_READY_STATE_LOADING)
3159 );
3160 }
3161
@@ -3192,7 +3198,11 @@ export function registerSuspenseInstanceRetry(
3198 callback: () => void,
3199 ) {
3200 const ownerDocument = instance.ownerDocument;
3195 - if (
3201 + if (instance.data === SUSPENSE_QUEUED_START_DATA) {
3202 + // The Fizz runtime has already queued this boundary for reveal. We wait for it
3203 + // to be revealed and then retries.
3204 + instance._reactRetry = callback;
3205 + } else if (
3206 // The Fizz runtime must have put this boundary into client render or complete
3207 // state after the render finished but before it committed. We need to call the
3208 // callback now rather than wait
@@ -3200,7 +3210,7 @@ export function registerSuspenseInstanceRetry(
3210 // The boundary is still in pending status but the document has finished loading
3211 // before we could register the event handler that would have scheduled the retry
3212 // on load so we call teh callback now.
3203 - ownerDocument.readyState === DOCUMENT_READY_STATE_COMPLETE
3213 + ownerDocument.readyState !== DOCUMENT_READY_STATE_LOADING
3214 ) {
3215 callback();
3216 } else {
@@ -3255,18 +3265,19 @@ function getNextHydratable(node: ?Node) {
3265 break;
3266 }
3267 if (nodeType === COMMENT_NODE) {
3258 - const nodeData = (node: any).data;
3268 + const data = (node: any).data;
3269 if (
3260 - nodeData === SUSPENSE_START_DATA ||
3261 - nodeData === SUSPENSE_FALLBACK_START_DATA ||
3262 - nodeData === SUSPENSE_PENDING_START_DATA ||
3263 - nodeData === ACTIVITY_START_DATA ||
3264 - nodeData === FORM_STATE_IS_MATCHING ||
3265 - nodeData === FORM_STATE_IS_NOT_MATCHING
3270 + data === SUSPENSE_START_DATA ||
3271 + data === SUSPENSE_FALLBACK_START_DATA ||
3272 + data === SUSPENSE_PENDING_START_DATA ||
3273 + data === SUSPENSE_QUEUED_START_DATA ||
3274 + data === ACTIVITY_START_DATA ||
3275 + data === FORM_STATE_IS_MATCHING ||
3276 + data === FORM_STATE_IS_NOT_MATCHING
3277 ) {
3278 break;
3279 }
3269 - if (nodeData === SUSPENSE_END_DATA || nodeData === ACTIVITY_END_DATA) {
3280 + if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
3281 return null;
3282 }
3283 }
@@ -3494,6 +3505,7 @@ function getNextHydratableInstanceAfterHydrationBoundary(
3505 data === SUSPENSE_START_DATA ||
3506 data === SUSPENSE_FALLBACK_START_DATA ||
3507 data === SUSPENSE_PENDING_START_DATA ||
3508 + data === SUSPENSE_QUEUED_START_DATA ||
3509 data === ACTIVITY_START_DATA
3510 ) {
3511 depth++;
@@ -3535,6 +3547,7 @@ export function getParentHydrationBoundary(
3547 data === SUSPENSE_START_DATA ||
3548 data === SUSPENSE_FALLBACK_START_DATA ||
3549 data === SUSPENSE_PENDING_START_DATA ||
3550 + data === SUSPENSE_QUEUED_START_DATA ||
3551 data === ACTIVITY_START_DATA
3552 ) {
3553 if (depth === 0) {
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js
+2 -2
@@ -6,9 +6,9 @@ export const markShellTime =
6 export const clientRenderBoundary =
7 '$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())};';
8 export const completeBoundary =
9 - '$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())};';
9 + '$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())};';
10 export const completeBoundaryWithStyles =
11 - '$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"))};';
11 + '$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"))};';
12 export const completeSegment =
13 '$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)};';
14 export const formReplaying =
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js
+14
@@ -9,6 +9,7 @@ const ACTIVITY_END_DATA = '/&';
9 const SUSPENSE_START_DATA = '$';
10 const SUSPENSE_END_DATA = '/$';
11 const SUSPENSE_PENDING_START_DATA = '$?';
12 +const SUSPENSE_QUEUED_START_DATA = '$~';
13 const SUSPENSE_FALLBACK_START_DATA = '$!';
14
15 // TODO: Symbols that are referenced outside this module use dynamic accessor
@@ -106,6 +107,7 @@ export function completeBoundary(suspenseBoundaryID, contentID) {
107 } else if (
108 data === SUSPENSE_START_DATA ||
109 data === SUSPENSE_PENDING_START_DATA ||
110 + data === SUSPENSE_QUEUED_START_DATA ||
111 data === SUSPENSE_FALLBACK_START_DATA ||
112 data === ACTIVITY_START_DATA
113 ) {
@@ -132,6 +134,10 @@ export function completeBoundary(suspenseBoundaryID, contentID) {
134 }
135 }
136
137 + // Mark this Suspense boundary as queued so we know not to client render it
138 + // at the end of document load.
139 + const suspenseNodeOuter = suspenseIdNodeOuter.previousSibling;
140 + suspenseNodeOuter.data = SUSPENSE_QUEUED_START_DATA;
141 // Queue this boundary for the next batch
142 window['$RB'].push(suspenseIdNodeOuter, contentNodeOuter);
143
@@ -261,6 +267,14 @@ export function completeBoundaryWithStyles(
267 }
268 }
269
270 + const suspenseIdNodeOuter = document.getElementById(suspenseBoundaryID);
271 + if (suspenseIdNodeOuter) {
272 + // Mark this Suspense boundary as queued so we know not to client render it
273 + // at the end of document load.
274 + const suspenseNodeOuter = suspenseIdNodeOuter.previousSibling;
275 + suspenseNodeOuter.data = SUSPENSE_QUEUED_START_DATA;
276 + }
277 +
278 Promise.all(dependencies).then(
279 window['$RC'].bind(null, suspenseBoundaryID, contentID),
280 window['$RX'].bind(null, suspenseBoundaryID, 'CSS failed to load'),