From ee077b6ccddd916440bb7570ba6f53673d920700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 1 May 2025 15:44:17 -0400 Subject: [PATCH 1/4] [Fizz] Don't handle errors in completeBoundary instruction (#33073) Stacked on #33066 and #33068. Currently we're passing `errorDigest` to `completeBoundary` if there is a client side error (only CSS loading atm). This only exists because of `completeBoundaryWithStyles`. Normally if there's a server-side error we'd emit the `clientRenderBoundary` instruction instead. This adds unnecessary code to the common case where all styles are in the head. This is about to get worse with batching because client render shouldn't be throttled but complete should be. The first commit moves the client render logic inline into `completeBoundaryWithStyles` so we only pay for it when styles are used. However, the approach I went with in the second commit is to reuse the `$RX` instruction instead (`clientRenderBoundary`). That way if you have both it ends up being amortized. However, it does mean we have to emit the `$RX` (along with the `$RC` helper if any `completeBoundaryWithStyles` instruction is needed. --- .../src/server/ReactFizzConfigDOM.js | 27 ++++-- ...tDOMFizzInstructionSetInlineCodeStrings.js | 4 +- .../ReactDOMFizzInstructionSetShared.js | 84 ++++++++----------- .../ReactDOMFizzStaticBrowser-test.js | 6 +- .../src/__tests__/ReactDOMFloat-test.js | 6 +- 5 files changed, 63 insertions(+), 64 deletions(-) diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 93b7b9ed9b071..bc3a7c36eb7db 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -4482,14 +4482,14 @@ export function writeCompletedSegmentInstruction( } } +const completeBoundaryScriptFunctionOnly = stringToPrecomputedChunk( + completeBoundaryFunction, +); const completeBoundaryScript1Full = stringToPrecomputedChunk( completeBoundaryFunction + '$RC("', ); const completeBoundaryScript1Partial = stringToPrecomputedChunk('$RC("'); -const completeBoundaryWithStylesScript1FullBoth = stringToPrecomputedChunk( - completeBoundaryFunction + styleInsertionFunction + '$RR("', -); const completeBoundaryWithStylesScript1FullPartial = stringToPrecomputedChunk( styleInsertionFunction + '$RR("', ); @@ -4531,19 +4531,27 @@ export function writeCompletedBoundaryInstruction( writeChunk(destination, renderState.startInlineScript); writeChunk(destination, endOfStartTag); if (requiresStyleInsertion) { + if ( + (resumableState.instructions & SentClientRenderFunction) === + NothingSent + ) { + // The completeBoundaryWithStyles function depends on the client render function. + resumableState.instructions |= SentClientRenderFunction; + writeChunk(destination, clientRenderScriptFunctionOnly); + } if ( (resumableState.instructions & SentCompleteBoundaryFunction) === NothingSent ) { - resumableState.instructions |= - SentStyleInsertionFunction | SentCompleteBoundaryFunction; - writeChunk(destination, completeBoundaryWithStylesScript1FullBoth); - } else if ( + // The completeBoundaryWithStyles function depends on the complete boundary function. + resumableState.instructions |= SentCompleteBoundaryFunction; + writeChunk(destination, completeBoundaryScriptFunctionOnly); + } + if ( (resumableState.instructions & SentStyleInsertionFunction) === NothingSent ) { resumableState.instructions |= SentStyleInsertionFunction; - writeChunk(destination, completeBoundaryWithStylesScript1FullPartial); } else { writeChunk(destination, completeBoundaryWithStylesScript1Partial); @@ -4608,6 +4616,9 @@ export function writeCompletedBoundaryInstruction( return writeBootstrap(destination, renderState) && writeMore; } +const clientRenderScriptFunctionOnly = + stringToPrecomputedChunk(clientRenderFunction); + const clientRenderScript1Full = stringToPrecomputedChunk( clientRenderFunction + ';$RX("', ); diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js index 7a437c07c622b..97a1a7b80a99b 100644 --- a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js @@ -4,9 +4,9 @@ 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 = - '$RC=function(b,d,e){if(d=document.getElementById(d)){d.parentNode.removeChild(d);var a=document.getElementById(b);if(a){b=a.previousSibling;if(e)b.data="$!",a.setAttribute("data-dgst",e);else{e=b.parentNode;a=b.nextSibling;var f=0;do{if(a&&8===a.nodeType){var c=a.data;if("/$"===c||"/&"===c)if(0===f)break;else f--;else"$"!==c&&"$?"!==c&&"$!"!==c&&"&"!==c||f++}c=a.nextSibling;e.removeChild(a);a=c}while(a);for(;d.firstChild;)e.insertBefore(d.firstChild,a);b.data="$"}b._reactRetry&&b._reactRetry()}}};'; + '$RC=function(a,d){if(d=document.getElementById(d))if(d.parentNode.removeChild(d),a=document.getElementById(a)){a=a.previousSibling;var f=a.parentNode,b=a.nextSibling,e=0;do{if(b&&8===b.nodeType){var c=b.data;if("/$"===c||"/&"===c)if(0===e)break;else e--;else"$"!==c&&"$?"!==c&&"$!"!==c&&"&"!==c||e++}c=b.nextSibling;f.removeChild(b);b=c}while(b);for(;d.firstChild;)f.insertBefore(d.firstChild,b);a.data="$";a._reactRetry&&a._reactRetry()}};'; export const completeBoundaryWithStyles = - '$RM=new Map;\n$RR=function(r,t,w){function u(n){this._p=null;n()}for(var p=new Map,q=document,g,b,h=q.querySelectorAll("link[data-precedence],style[data-precedence]"),v=[],k=0;b=h[k++];)"not all"===b.getAttribute("media")?v.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=u.bind(a,n);a.onerror=u.bind(a,x)});$RM.set(d,a)}d=a.getAttribute("media");!f||d&&!matchMedia(d).matches||h.push(f);if(c)continue}else{a=v[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,t,""),$RC.bind(null,r,t,"Resource failed to load"))};'; + '$RM=new Map;\n$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"))};'; 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 = diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js index 6e3d15fea1934..6c21d245b73df 100644 --- a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js @@ -47,7 +47,7 @@ export function clientRenderBoundary( } } -export function completeBoundary(suspenseBoundaryID, contentID, errorDigest) { +export function completeBoundary(suspenseBoundaryID, contentID) { const contentNode = document.getElementById(contentID); if (!contentNode) { // If the client has failed hydration we may have already deleted the streaming @@ -70,52 +70,47 @@ export function completeBoundary(suspenseBoundaryID, contentID, errorDigest) { // Find the boundary around the fallback. This is always the previous node. const suspenseNode = suspenseIdNode.previousSibling; - if (!errorDigest) { - // Clear all the existing children. This is complicated because - // there can be embedded Suspense boundaries in the fallback. - // This is similar to clearSuspenseBoundary in ReactFiberConfigDOM. - // TODO: We could avoid this if we never emitted suspense boundaries in fallback trees. - // They never hydrate anyway. However, currently we support incrementally loading the fallback. - const parentInstance = suspenseNode.parentNode; - let node = suspenseNode.nextSibling; - let depth = 0; - do { - if (node && node.nodeType === COMMENT_NODE) { - const data = node.data; - if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) { - if (depth === 0) { - break; - } else { - depth--; - } - } else if ( - data === SUSPENSE_START_DATA || - data === SUSPENSE_PENDING_START_DATA || - data === SUSPENSE_FALLBACK_START_DATA || - data === ACTIVITY_START_DATA - ) { - depth++; + // Clear all the existing children. This is complicated because + // there can be embedded Suspense boundaries in the fallback. + // This is similar to clearSuspenseBoundary in ReactFiberConfigDOM. + // TODO: We could avoid this if we never emitted suspense boundaries in fallback trees. + // They never hydrate anyway. However, currently we support incrementally loading the fallback. + const parentInstance = suspenseNode.parentNode; + let node = suspenseNode.nextSibling; + let depth = 0; + do { + if (node && node.nodeType === COMMENT_NODE) { + const data = node.data; + if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) { + if (depth === 0) { + break; + } else { + depth--; } + } else if ( + data === SUSPENSE_START_DATA || + data === SUSPENSE_PENDING_START_DATA || + data === SUSPENSE_FALLBACK_START_DATA || + data === ACTIVITY_START_DATA + ) { + depth++; } + } - const nextNode = node.nextSibling; - parentInstance.removeChild(node); - node = nextNode; - } while (node); - - const endOfBoundary = node; + const nextNode = node.nextSibling; + parentInstance.removeChild(node); + node = nextNode; + } while (node); - // Insert all the children from the contentNode between the start and end of suspense boundary. - while (contentNode.firstChild) { - parentInstance.insertBefore(contentNode.firstChild, endOfBoundary); - } + const endOfBoundary = node; - suspenseNode.data = SUSPENSE_START_DATA; - } else { - suspenseNode.data = SUSPENSE_FALLBACK_START_DATA; - suspenseIdNode.setAttribute('data-dgst', errorDigest); + // Insert all the children from the contentNode between the start and end of suspense boundary. + while (contentNode.firstChild) { + parentInstance.insertBefore(contentNode.firstChild, endOfBoundary); } + suspenseNode.data = SUSPENSE_START_DATA; + if (suspenseNode['_reactRetry']) { suspenseNode['_reactRetry'](); } @@ -234,13 +229,8 @@ export function completeBoundaryWithStyles( } Promise.all(dependencies).then( - window['$RC'].bind(null, suspenseBoundaryID, contentID, ''), - window['$RC'].bind( - null, - suspenseBoundaryID, - contentID, - 'Resource failed to load', - ), + window['$RC'].bind(null, suspenseBoundaryID, contentID), + window['$RX'].bind(null, suspenseBoundaryID, 'CSS failed to load'), ); } diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js index 7eecb16cf82f6..2f63c9695b5d8 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js @@ -1630,14 +1630,14 @@ describe('ReactDOMFizzStaticBrowser', () => { // We are mostly just trying to assert that no preload for our stylesheet was emitted // prior to sending the segment the stylesheet was for. This test is asserting this // because the boundary complete instruction is sent when we are writing the - const instructionIndex = result.indexOf('$RC'); + const instructionIndex = result.indexOf('$RX'); expect(instructionIndex > -1).toBe(true); - const slice = result.slice(0, instructionIndex + '$RC'.length); + const slice = result.slice(0, instructionIndex + '$RX'.length); expect(slice).toBe( '' + 'hello' + - '' + content, + { + runScripts: 'dangerously', + }, + ); const originalWindow = global.window; const originalDocument = global.document; const originalNavigator = global.navigator; @@ -167,6 +175,7 @@ describe('ReactDOMFizzStaticBrowser', () => { const temp = document.createElement('div'); temp.innerHTML = content; await insertNodesAndExecuteScripts(temp, document.body, null); + jest.runAllTimers(); } it('should call prerender', async () => { @@ -980,6 +989,7 @@ describe('ReactDOMFizzStaticBrowser', () => { // Wait for the instruction microtasks to flush. await 0; await 0; + jest.runAllTimers(); expect(getVisibleChildren(container)).toEqual([ , @@ -1611,7 +1621,7 @@ describe('ReactDOMFizzStaticBrowser', () => { expect(result).toBe( '' + - 'hello', + 'hello', ); await 1; @@ -1636,7 +1646,7 @@ describe('ReactDOMFizzStaticBrowser', () => { expect(slice).toBe( '' + - 'hello' + + 'hello' + '']); }); @@ -3609,6 +3618,7 @@ body { assertConsoleErrorDev([ "Hydration failed because the server rendered HTML didn't match the client.", ]); + jest.runAllTimers(); expect(getMeaningfulChildren(document)).toEqual( @@ -5202,6 +5212,10 @@ body { , ); loadStylesheets(); + // Let the styles flush and then flush the boundaries + await 0; + await 0; + jest.runAllTimers(); assertLog([ 'load stylesheet: shell preinit/shell', 'load stylesheet: shell/shell preinit', diff --git a/packages/react-markup/src/ReactFizzConfigMarkup.js b/packages/react-markup/src/ReactFizzConfigMarkup.js index 444952dc58502..800dd46e0ad77 100644 --- a/packages/react-markup/src/ReactFizzConfigMarkup.js +++ b/packages/react-markup/src/ReactFizzConfigMarkup.js @@ -224,6 +224,7 @@ export function writeCompletedRoot( destination: Destination, resumableState: ResumableState, renderState: RenderState, + isComplete: boolean, ): boolean { // Markup doesn't have any bootstrap scripts nor shell completions. return true; diff --git a/packages/react-noop-renderer/src/ReactNoopServer.js b/packages/react-noop-renderer/src/ReactNoopServer.js index fe00f28b0240f..119c9885db783 100644 --- a/packages/react-noop-renderer/src/ReactNoopServer.js +++ b/packages/react-noop-renderer/src/ReactNoopServer.js @@ -55,6 +55,7 @@ type Destination = { stack: Array, }; +type ResumableState = null; type RenderState = null; type HoistableState = null; type PreambleState = null; @@ -153,7 +154,9 @@ const ReactNoopServer = ReactFizzServer({ writeCompletedRoot( destination: Destination, + resumableState: ResumableState, renderState: RenderState, + isComplete: boolean, ): boolean { return true; }, diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index f55d6f08043f4..65efe7c7e8935 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -5217,10 +5217,18 @@ function flushCompletedQueues( ); flushSegment(request, destination, completedRootSegment, null); request.completedRootSegment = null; + const isComplete = + request.allPendingTasks === 0 && + request.clientRenderedBoundaries.length === 0 && + request.completedBoundaries.length === 0 && + (request.trackedPostpones === null || + (request.trackedPostpones.rootNodes.length === 0 && + request.trackedPostpones.rootSlots === null)); writeCompletedRoot( destination, request.resumableState, request.renderState, + isComplete, ); } @@ -5293,7 +5301,6 @@ function flushCompletedQueues( } finally { if ( request.allPendingTasks === 0 && - request.pingedTasks.length === 0 && request.clientRenderedBoundaries.length === 0 && request.completedBoundaries.length === 0 // We don't need to check any partially completed segments because diff --git a/scripts/rollup/generate-inline-fizz-runtime.js b/scripts/rollup/generate-inline-fizz-runtime.js index ce613fdb8d039..3d097f63e216a 100644 --- a/scripts/rollup/generate-inline-fizz-runtime.js +++ b/scripts/rollup/generate-inline-fizz-runtime.js @@ -13,6 +13,10 @@ const inlineCodeStringsFilename = instructionDir + '/ReactDOMFizzInstructionSetInlineCodeStrings.js'; const config = [ + { + entry: 'ReactDOMFizzInlineShellTime.js', + exportName: 'markShellTime', + }, { entry: 'ReactDOMFizzInlineClientRenderBoundary.js', exportName: 'clientRenderBoundary', @@ -66,7 +70,7 @@ async function main() { }); }); - return `export const ${exportName} = ${JSON.stringify(code.trim())};`; + return `export const ${exportName} = ${JSON.stringify(code.trim().replace('\n', ''))};`; }) ); From 0ed6ceb9f6c19f28c504cf46193cef40166a61f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 1 May 2025 16:11:54 -0400 Subject: [PATCH 3/4] [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. --- .../src/client/ReactFiberConfigDOM.js | 39 ++++++++++++------- ...tDOMFizzInstructionSetInlineCodeStrings.js | 4 +- .../ReactDOMFizzInstructionSetShared.js | 14 +++++++ 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index ad0477232ca02..e1cd27b69bb23 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -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) { diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js index 0653761df7594..470698b27c0c5 100644 --- a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js @@ -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 Date: Thu, 1 May 2025 18:14:42 -0400 Subject: [PATCH 4/4] [Fizz] Always load the external runtime if one is provided (#33091) Because we now decided whether to outline in the flushing phase, when we're writing the preamble we don't yet know if we will make that decision so we don't know if it's safe to omit the external runtime. However, if you are providing an external runtime it's probably a pretty safe bet you're streaming something dynamically that's likely to need it so we can always include it. The main thing is that this makes it hard to test it because it affects our tests in ways it wouldn't otherwise so we have to add a bunch of conditions. --- .../src/server/ReactFizzConfigDOM.js | 7 +---- .../src/__tests__/ReactDOMFizzServer-test.js | 29 ++++++++++++++----- .../src/__tests__/ReactDOMFloat-test.js | 3 ++ .../react-markup/src/ReactFizzConfigMarkup.js | 2 -- packages/react-server/src/ReactFizzServer.js | 9 +----- 5 files changed, 26 insertions(+), 24 deletions(-) diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 371a1dd229430..a54a623bb3954 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -5089,15 +5089,10 @@ export function writePreambleStart( destination: Destination, resumableState: ResumableState, renderState: RenderState, - willFlushAllSegments: boolean, skipExpect?: boolean, // Used as an override by ReactFizzConfigMarkup ): void { // This function must be called exactly once on every request - if ( - enableFizzExternalRuntime && - !willFlushAllSegments && - renderState.externalRuntimeScript - ) { + if (enableFizzExternalRuntime && renderState.externalRuntimeScript) { // If the root segment is incomplete due to suspended tasks // (e.g. willFlushAllSegments = false) and we are using data // streaming format, ensure the external runtime is sent. diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 7f4300f687606..1be040bfe36c9 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -3587,6 +3587,9 @@ describe('ReactDOMFizzServer', () => { '' + + (gate(flags => flags.shouldUseFizzExternalRuntime) + ? '' + : '') + '', ); }); @@ -4501,7 +4504,8 @@ describe('ReactDOMFizzServer', () => { expect(document.getElementsByTagName('script').length).toEqual(1); }); - it('does not send the external runtime for static pages', async () => { + // @gate shouldUseFizzExternalRuntime + it('does (unfortunately) send the external runtime for static pages', async () => { await act(() => { const {pipe} = renderToPipeableStream( @@ -4515,11 +4519,11 @@ describe('ReactDOMFizzServer', () => { }); // no scripts should be sent - expect(document.getElementsByTagName('script').length).toEqual(0); + expect(document.getElementsByTagName('script').length).toEqual(1); // the html should be as-is expect(document.documentElement.innerHTML).toEqual( - '

hello world!

', + '

hello world!

', ); }); @@ -5317,7 +5321,9 @@ describe('ReactDOMFizzServer', () => { }); expect(container.innerHTML).toEqual( - '
helloworld, Foo!
', + (gate(flags => flags.shouldUseFizzExternalRuntime) + ? '' + : '') + '
helloworld, Foo!
', ); const errors = []; ReactDOMClient.hydrateRoot(container, , { @@ -5518,7 +5524,7 @@ describe('ReactDOMFizzServer', () => { pipe(writable); }); - expect(container.firstElementChild.outerHTML).toEqual( + expect(container.lastElementChild.outerHTML).toEqual( '
helloworld
', ); @@ -5556,7 +5562,7 @@ describe('ReactDOMFizzServer', () => { pipe(writable); }); - expect(container.firstElementChild.outerHTML).toEqual( + expect(container.lastElementChild.outerHTML).toEqual( '
helloworld
', ); @@ -5696,7 +5702,10 @@ describe('ReactDOMFizzServer', () => { }); expect(container.innerHTML).toEqual( - '
helloworldworldhelloworld
world
', + (gate(flags => flags.shouldUseFizzExternalRuntime) + ? '' + : '') + + '
helloworldworldhelloworld
world
', ); const errors = []; @@ -6499,7 +6508,11 @@ describe('ReactDOMFizzServer', () => { }); expect(document.documentElement.outerHTML).toEqual( - '', + '' + + (gate(flags => flags.shouldUseFizzExternalRuntime) + ? '' + : '') + + '', ); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index 2b7a71b7c7b14..b7f42bcf8dbef 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -701,6 +701,9 @@ describe('ReactDOMFloat', () => { }); expect(chunks).toEqual([ '' + + (gate(flags => flags.shouldUseFizzExternalRuntime) + ? '' + : '') + 'foo' + 'bar', '', diff --git a/packages/react-markup/src/ReactFizzConfigMarkup.js b/packages/react-markup/src/ReactFizzConfigMarkup.js index 800dd46e0ad77..7d14ccd628de2 100644 --- a/packages/react-markup/src/ReactFizzConfigMarkup.js +++ b/packages/react-markup/src/ReactFizzConfigMarkup.js @@ -208,14 +208,12 @@ export function writePreambleStart( destination: Destination, resumableState: ResumableState, renderState: RenderState, - willFlushAllSegments: boolean, skipExpect?: boolean, // Used as an override by ReactFizzConfigMarkup ): void { return writePreambleStartImpl( destination, resumableState, renderState, - willFlushAllSegments, true, // skipExpect ); } diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 65efe7c7e8935..155bb7fe3ff3a 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -4835,14 +4835,7 @@ function flushPreamble( preambleSegments: Array>, ) { // The preamble is ready. - const willFlushAllSegments = - request.allPendingTasks === 0 && request.trackedPostpones === null; - writePreambleStart( - destination, - request.resumableState, - request.renderState, - willFlushAllSegments, - ); + writePreambleStart(destination, request.resumableState, request.renderState); for (let i = 0; i < preambleSegments.length; i++) { const segments = preambleSegments[i]; for (let j = 0; j < segments.length; j++) {