diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 067f668262be4..c5eeaa5809b78 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -147,9 +147,12 @@ export type RenderState = { // external runtime script chunks externalRuntimeScript: null | ExternalRuntimeScript, bootstrapChunks: Array, + importMapChunks: Array, // Hoistable chunks - importMapChunks: Array, + charsetChunks: Array, + viewportChunks: Array, + hoistableChunks: Array, // Headers queues for Resources that can flush early onHeaders: void | ((headers: HeadersDescriptor) => void), @@ -466,6 +469,10 @@ export function createRenderState( style: {}, }, + charsetChunks: [], + viewportChunks: [], + hoistableChunks: [], + // cleared on flush preconnects: new Set(), fontPreloads: new Set(), @@ -485,6 +492,7 @@ export function createRenderState( nonce, // like a module global for currently rendering boundary + hoistableState: null, stylesToHoist: false, }; @@ -2213,7 +2221,8 @@ function pushStartTextArea( function pushMeta( target: Array, props: Object, - hoistableState: HoistableState, + renderState: RenderState, + hoistableState: null | HoistableState, textEmbedded: boolean, insertionMode: InsertionMode, noscriptTagInScope: boolean, @@ -2233,12 +2242,30 @@ function pushMeta( } if (typeof props.charSet === 'string') { - return pushSelfClosing(hoistableState.charset, props, 'meta'); + return pushSelfClosing( + hoistableState + ? hoistableState.charsetChunks + : renderState.charsetChunks, + props, + 'meta', + ); } else if (props.name === 'viewport') { // "viewport" isn't related to preconnect but it has the right priority - return pushSelfClosing(hoistableState.viewport, props, 'meta'); + return pushSelfClosing( + hoistableState + ? hoistableState.viewportChunks + : renderState.viewportChunks, + props, + 'meta', + ); } else { - return pushSelfClosing(hoistableState.chunks, props, 'meta'); + return pushSelfClosing( + hoistableState + ? hoistableState.hoistableChunks + : renderState.hoistableChunks, + props, + 'meta', + ); } } } else { @@ -2251,8 +2278,7 @@ function pushLink( props: Object, resumableState: ResumableState, renderState: RenderState, - boundaryResources: null | BoundaryResources, - hoistableState: HoistableState, + hoistableState: null | HoistableState, textEmbedded: boolean, insertionMode: InsertionMode, noscriptTagInScope: boolean, @@ -2373,8 +2399,8 @@ function pushLink( // We add the newly created resource to our StyleQueue and if necessary // track the resource with the currently rendering boundary styleQueue.sheets.set(key, resource); - if (boundaryResources) { - boundaryResources.stylesheets.add(resource); + if (hoistableState) { + hoistableState.stylesheets.add(resource); } } else { // We need to track whether this boundary should wait on this resource or not. @@ -2385,8 +2411,8 @@ function pushLink( if (styleQueue) { const resource = styleQueue.sheets.get(key); if (resource) { - if (boundaryResources) { - boundaryResources.stylesheets.add(resource); + if (hoistableState) { + hoistableState.stylesheets.add(resource); } } } @@ -2411,7 +2437,10 @@ function pushLink( target.push(textSeparator); } - return pushLinkImpl(hoistableState.chunks, props); + const hoistableChunks = hoistableState + ? hoistableState.hoistableChunks + : renderState.hoistableChunks; + return pushLinkImpl(hoistableChunks, props); } } else { return pushLinkImpl(target, props); @@ -2453,7 +2482,7 @@ function pushStyle( props: Object, resumableState: ResumableState, renderState: RenderState, - boundaryResources: null | BoundaryResources, + hoistableState: null | HoistableState, textEmbedded: boolean, insertionMode: InsertionMode, noscriptTagInScope: boolean, @@ -2553,8 +2582,8 @@ function pushStyle( // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. - if (boundaryResources) { - boundaryResources.styles.add(styleQueue); + if (hoistableState) { + hoistableState.styles.add(styleQueue); } } @@ -2864,7 +2893,8 @@ function pushStartMenuItem( function pushTitle( target: Array, props: Object, - hoistableState: HoistableState, + renderState: RenderState, + hoistableState: null | HoistableState, insertionMode: InsertionMode, noscriptTagInScope: boolean, ): ReactNodeList { @@ -2922,7 +2952,10 @@ function pushTitle( !noscriptTagInScope && props.itemProp == null ) { - pushTitleImpl(hoistableState.chunks, props); + const hoistableTarget = hoistableState + ? hoistableState.hoistableChunks + : renderState.hoistableChunks; + pushTitleImpl(hoistableTarget, props); return null; } else { return pushTitleImpl(target, props); @@ -3454,8 +3487,7 @@ export function pushStartInstance( props: Object, resumableState: ResumableState, renderState: RenderState, - boundaryResources: null | BoundaryResources, - hoistableState: HoistableState, + hoistableState: null | HoistableState, formatContext: FormatContext, textEmbedded: boolean, ): ReactNodeList { @@ -3523,6 +3555,7 @@ export function pushStartInstance( ? pushTitle( target, props, + renderState, hoistableState, formatContext.insertionMode, !!(formatContext.tagScope & NOSCRIPT_SCOPE), @@ -3534,7 +3567,6 @@ export function pushStartInstance( props, resumableState, renderState, - boundaryResources, hoistableState, textEmbedded, formatContext.insertionMode, @@ -3558,7 +3590,7 @@ export function pushStartInstance( props, resumableState, renderState, - boundaryResources, + hoistableState, textEmbedded, formatContext.insertionMode, !!(formatContext.tagScope & NOSCRIPT_SCOPE), @@ -3567,6 +3599,7 @@ export function pushStartInstance( return pushMeta( target, props, + renderState, hoistableState, textEmbedded, formatContext.insertionMode, @@ -4107,7 +4140,7 @@ export function writeCompletedBoundaryInstruction( resumableState: ResumableState, renderState: RenderState, id: number, - boundaryResources: BoundaryResources, + hoistableState: HoistableState, ): boolean { let requiresStyleInsertion; if (enableFloat) { @@ -4183,11 +4216,11 @@ export function writeCompletedBoundaryInstruction( // e.g. ["A", "B"] if (scriptFormat) { writeChunk(destination, completeBoundaryScript3a); - // boundaryResources encodes an array literal - writeStyleResourceDependenciesInJS(destination, boundaryResources); + // hoistableState encodes an array literal + writeStyleResourceDependenciesInJS(destination, hoistableState); } else { writeChunk(destination, completeBoundaryData3a); - writeStyleResourceDependenciesInAttr(destination, boundaryResources); + writeStyleResourceDependenciesInAttr(destination, hoistableState); } } else { if (scriptFormat) { @@ -4436,9 +4469,35 @@ function hasStylesToHoist(stylesheet: StylesheetResource): boolean { return false; } -export function writeResourcesForBoundary( +export function writeHoistablesForPartialBoundary( + destination: Destination, + hoistableState: HoistableState, + renderState: RenderState, +): boolean { + // Reset these on each invocation, they are only safe to read in this function + currentlyRenderingBoundaryHasStylesToHoist = false; + destinationHasCapacity = true; + + // Flush style tags for each precedence this boundary depends on + hoistableState.styles.forEach(flushStyleTagsLateForBoundary, destination); + + // Determine if this boundary has stylesheets that need to be awaited upon completion + hoistableState.stylesheets.forEach(hasStylesToHoist); + + // We don't actually want to flush any hoistables until the boundary is complete so we omit + // any further writing here. This is becuase unlike Resources, Hoistable Elements act more like + // regular elements, each rendered element has a unique representation in the DOM. We don't want + // these elements to appear in the DOM early, before the boundary has actually completed + + if (currentlyRenderingBoundaryHasStylesToHoist) { + renderState.stylesToHoist = true; + } + return destinationHasCapacity; +} + +export function writeHoistablesForCompletedBoundary( destination: Destination, - boundaryResources: BoundaryResources, + hoistableState: HoistableState, renderState: RenderState, ): boolean { // Reset these on each invocation, they are only safe to read in this function @@ -4446,10 +4505,40 @@ export function writeResourcesForBoundary( destinationHasCapacity = true; // Flush style tags for each precedence this boundary depends on - boundaryResources.styles.forEach(flushStyleTagsLateForBoundary, destination); + hoistableState.styles.forEach(flushStyleTagsLateForBoundary, destination); // Determine if this boundary has stylesheets that need to be awaited upon completion - boundaryResources.stylesheets.forEach(hasStylesToHoist); + hoistableState.stylesheets.forEach(hasStylesToHoist); + + // Flush Hoistable Elements + let i; + const charsetChunks = hoistableState.charsetChunks; + for (i = 0; i < charsetChunks.length - 1; i++) { + writeChunk(destination, charsetChunks[i]); + } + if (i < charsetChunks.length) { + destinationHasCapacity = writeChunkAndReturn(destination, charsetChunks[i]); + } + const viewportChunks = hoistableState.viewportChunks; + for (i = 0; i < viewportChunks.length - 1; i++) { + writeChunk(destination, charsetChunks[i]); + } + if (i < viewportChunks.length) { + destinationHasCapacity = writeChunkAndReturn( + destination, + viewportChunks[i], + ); + } + const hoistableChunks = hoistableState.hoistableChunks; + for (i = 0; i < hoistableChunks.length - 1; i++) { + writeChunk(destination, hoistableChunks[i]); + } + if (i < hoistableChunks.length) { + destinationHasCapacity = writeChunkAndReturn( + destination, + hoistableChunks[i], + ); + } if (currentlyRenderingBoundaryHasStylesToHoist) { renderState.stylesToHoist = true; @@ -4561,7 +4650,6 @@ export function writePreamble( destination: Destination, resumableState: ResumableState, renderState: RenderState, - hoistableState: HoistableState, willFlushAllSegments: boolean, ): void { // This function must be called exactly once on every request @@ -4607,7 +4695,7 @@ export function writePreamble( } // Emit high priority Hoistables - const charsetChunks = hoistableState.charset; + const charsetChunks = renderState.charsetChunks; for (i = 0; i < charsetChunks.length; i++) { writeChunk(destination, charsetChunks[i]); } @@ -4617,7 +4705,7 @@ export function writePreamble( renderState.preconnects.forEach(flushResource, destination); renderState.preconnects.clear(); - const viewportChunks = hoistableState.viewport; + const viewportChunks = renderState.viewportChunks; for (i = 0; i < viewportChunks.length; i++) { writeChunk(destination, viewportChunks[i]); } @@ -4646,8 +4734,8 @@ export function writePreamble( renderState.bulkPreloads.forEach(flushResource, destination); renderState.bulkPreloads.clear(); - // Write hoistableState chunks - const hoistableChunks = hoistableState.chunks; + // Write embedding hoistableChunks + const hoistableChunks = renderState.hoistableChunks; for (i = 0; i < hoistableChunks.length; i++) { writeChunk(destination, hoistableChunks[i]); } @@ -4664,23 +4752,15 @@ export function writePreamble( } } -// We don't bother reporting backpressure at the moment because we expect to -// flush the entire preamble in a single pass. This probably should be modified -// in the future to be backpressure sensitive but that requires a larger refactor -// of the flushing code in Fizz. +// This is an opportunity to write hoistables however in the current implemention +// the only hoistables that make sense to write here are Resources. Hoistable Elements +// would have already been written as part of the preamble or will be written as part +// of a boundary completion and thus don't need to be written here. export function writeHoistables( destination: Destination, resumableState: ResumableState, renderState: RenderState, - hoistableState: HoistableState, ): void { - let i = 0; - - // Emit high priority Hoistables - - // We omit charsetChunks because we have already sent the shell and if it wasn't - // already sent it is too late now. - renderState.preconnects.forEach(flushResource, destination); renderState.preconnects.clear(); @@ -4707,13 +4787,6 @@ export function writeHoistables( renderState.bulkPreloads.forEach(flushResource, destination); renderState.bulkPreloads.clear(); - - // Write hoistableState chunks - const hoistableChunks = hoistableState.chunks; - for (i = 0; i < hoistableChunks.length; i++) { - writeChunk(destination, hoistableChunks[i]); - } - hoistableChunks.length = 0; } export function writePostamble( @@ -4738,12 +4811,12 @@ const arrayCloseBracket = stringToPrecomputedChunk(']'); // [["JS_escaped_string1", "JS_escaped_string2"]] function writeStyleResourceDependenciesInJS( destination: Destination, - boundaryResources: BoundaryResources, + hoistableState: HoistableState, ): void { writeChunk(destination, arrayFirstOpenBracket); let nextArrayOpenBrackChunk = arrayFirstOpenBracket; - boundaryResources.stylesheets.forEach(resource => { + hoistableState.stylesheets.forEach(resource => { if (resource.state === PREAMBLE) { // We can elide this dependency because it was flushed in the shell and // should be ready before content is shown on the client @@ -4931,12 +5004,12 @@ function writeStyleResourceAttributeInJS( // [["JSON_escaped_string1", "JSON_escaped_string2"]] function writeStyleResourceDependenciesInAttr( destination: Destination, - boundaryResources: BoundaryResources, + hoistableState: HoistableState, ): void { writeChunk(destination, arrayFirstOpenBracket); let nextArrayOpenBrackChunk = arrayFirstOpenBracket; - boundaryResources.stylesheets.forEach(resource => { + hoistableState.stylesheets.forEach(resource => { if (resource.state === PREAMBLE) { // We can elide this dependency because it was flushed in the shell and // should be ready before content is shown on the client @@ -5184,23 +5257,12 @@ type StylesheetResource = { }; export type HoistableState = { - charset: Array, - viewport: Array, - chunks: Array, -}; - -export function createHoistableState(): HoistableState { - return { - charset: [], - viewport: [], - chunks: [], - }; -} - -export type BoundaryResources = { - // style dependencies styles: Set, stylesheets: Set, + // Hoistable chunks + charsetChunks: Array, + viewportChunks: Array, + hoistableChunks: Array, }; export type StyleQueue = { @@ -5210,10 +5272,13 @@ export type StyleQueue = { sheets: Map, }; -export function createBoundaryResources(): BoundaryResources { +export function createHoistableState(): HoistableState { return { styles: new Set(), stylesheets: new Set(), + charsetChunks: [], + viewportChunks: [], + hoistableChunks: [], }; } @@ -6063,35 +6128,58 @@ function escapeStringForLinkHeaderQuotedParamValueContextReplacer( } } -export function hoistHoistables( - target: HoistableState, - source: HoistableState, -) { - target.charset.push(...source.charset); - target.viewport.push(...source.viewport); - target.chunks.push(...source.chunks); -} - function hoistStyleQueueDependency( - this: BoundaryResources, + this: HoistableState, styleQueue: StyleQueue, ) { this.styles.add(styleQueue); } function hoistStylesheetDependency( - this: BoundaryResources, + this: HoistableState, stylesheet: StylesheetResource, ) { this.stylesheets.add(stylesheet); } -export function hoistBoundaryResources( - target: BoundaryResources, - source: BoundaryResources, +export function hoistToBoundary( + parentState: HoistableState, + childState: HoistableState, ): void { - source.styles.forEach(hoistStyleQueueDependency, target); - source.stylesheets.forEach(hoistStylesheetDependency, target); + childState.styles.forEach(hoistStyleQueueDependency, parentState); + childState.stylesheets.forEach(hoistStylesheetDependency, parentState); + let i; + const charsetChunks = childState.charsetChunks; + for (i = 0; i < charsetChunks.length; i++) { + parentState.charsetChunks.push(charsetChunks[i]); + } + const viewportChunks = childState.viewportChunks; + for (i = 0; i < charsetChunks.length; i++) { + parentState.viewportChunks.push(viewportChunks[i]); + } + const hoistableChunks = childState.hoistableChunks; + for (i = 0; i < hoistableChunks.length; i++) { + parentState.hoistableChunks.push(hoistableChunks[i]); + } +} + +export function hoistToRoot( + renderState: RenderState, + hoistableState: HoistableState, +): void { + let i; + const charsetChunks = hoistableState.charsetChunks; + for (i = 0; i < charsetChunks.length; i++) { + renderState.charsetChunks.push(charsetChunks[i]); + } + const viewportChunks = hoistableState.viewportChunks; + for (i = 0; i < charsetChunks.length; i++) { + renderState.viewportChunks.push(viewportChunks[i]); + } + const hoistableChunks = hoistableState.hoistableChunks; + for (i = 0; i < hoistableChunks.length; i++) { + renderState.hoistableChunks.push(hoistableChunks[i]); + } } // This function is called at various times depending on whether we are rendering diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js index fb0fef7b3cc14..35554654cd07f 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js @@ -56,6 +56,9 @@ export type RenderState = { remainingCapacity: number, }, resets: BaseRenderState['resets'], + charsetChunks: Array, + viewportChunks: Array, + hoistableChunks: Array, preconnects: Set, fontPreloads: Set, highImagePreloads: Set, @@ -101,6 +104,9 @@ export function createRenderState( onHeaders: renderState.onHeaders, headers: renderState.headers, resets: renderState.resets, + charsetChunks: renderState.charsetChunks, + viewportChunks: renderState.viewportChunks, + hoistableChunks: renderState.hoistableChunks, preconnects: renderState.preconnects, fontPreloads: renderState.fontPreloads, highImagePreloads: renderState.highImagePreloads, @@ -128,7 +134,6 @@ export const doctypeChunk: PrecomputedChunk = stringToPrecomputedChunk(''); export type { ResumableState, HoistableState, - BoundaryResources, FormatContext, } from './ReactFizzConfigDOM'; @@ -148,18 +153,18 @@ export { writeClientRenderBoundaryInstruction, writeStartPendingSuspenseBoundary, writeEndPendingSuspenseBoundary, - writeResourcesForBoundary, + writeHoistablesForPartialBoundary, + writeHoistablesForCompletedBoundary, writePlaceholder, writeCompletedRoot, createRootFormatContext, createResumableState, - createBoundaryResources, createHoistableState, writePreamble, writeHoistables, writePostamble, - hoistBoundaryResources, - hoistHoistables, + hoistToBoundary, + hoistToRoot, prepareHostDispatcher, resetResumableState, completeResumableState, diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index 7f64612f2b828..cb48446087912 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -7924,57 +7924,6 @@ background-color: green; ); }); - // @gate enableFloat - it('emits hoistables before other content when streaming in late', async () => { - let content = ''; - writable.on('data', chunk => (content += chunk)); - - await act(() => { - const {pipe} = renderToPipeableStream( - - - - - -
foo
- -
-
- - , - ); - pipe(writable); - }); - - expect(getMeaningfulChildren(document)).toEqual( - - - - - - , - ); - content = ''; - - await act(() => { - resolveText('foo'); - }); - - expect(content.slice(0, 30)).toEqual('
- - - - -
foo
- - - , - ); - }); - // @gate enableFloat it('supports rendering hoistables outside of scope', async () => { await act(() => { diff --git a/packages/react-noop-renderer/src/ReactNoopServer.js b/packages/react-noop-renderer/src/ReactNoopServer.js index 562ecffc47ffa..ae508385ba9b4 100644 --- a/packages/react-noop-renderer/src/ReactNoopServer.js +++ b/packages/react-noop-renderer/src/ReactNoopServer.js @@ -53,7 +53,6 @@ type Destination = { type RenderState = null; type HoistableState = null; -type BoundaryResources = null; const POP = Buffer.from('/', 'utf8'); @@ -262,24 +261,18 @@ const ReactNoopServer = ReactFizzServer({ boundary.status = 'client-render'; }, + prepareHostDispatcher() {}, + writePreamble() {}, writeHoistables() {}, writePostamble() {}, - - createBoundaryResources(): BoundaryResources { - return null; - }, - + hoistToRoot(renderState: RenderState, hoistableState: HoistableState) {}, + hoistToBoundary(parent: HoistableState, child: HoistableState) {}, createHoistableState(): HoistableState { return null; }, - - hoistHoistables( - parentHoistableState: HoistableState, - hoistableState: HoistableState, - ) {}, - - prepareHostDispatcher() {}, + writeHoistablesForPartialBoundary() {}, + writeHoistablesForCompleteBoundary() {}, emitEarlyPreloads() {}, }); diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index c4b15f61e5b37..ec59a382d3b5d 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -25,9 +25,8 @@ import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy'; import type { RenderState, ResumableState, - HoistableState, - BoundaryResources, FormatContext, + HoistableState, } from './ReactFizzConfig'; import type {ContextSnapshot} from './ReactFizzNewContext'; import type {ComponentStackNode} from './ReactFizzComponentStack'; @@ -65,13 +64,13 @@ import { pushEndCompletedSuspenseBoundary, pushSegmentFinale, getChildFormatContext, - writeResourcesForBoundary, - writePreamble, + writeHoistablesForPartialBoundary, + writeHoistablesForCompletedBoundary, writeHoistables, + writePreamble, writePostamble, - hoistBoundaryResources, - hoistHoistables, - createBoundaryResources, + hoistToBoundary, + hoistToRoot, createHoistableState, prepareHostDispatcher, supportsRequestStorage, @@ -211,10 +210,10 @@ type SuspenseBoundary = { parentFlushed: boolean, pendingTasks: number, // when it reaches zero we can show this boundary's content completedSegments: Array, // completed but not yet flushed segments. - fallbackHoistables: null | Hoistables, byteSize: number, // used to determine whether to inline children boundaries. fallbackAbortableTasks: Set, // used to cancel task on the fallback if the boundary completes or gets canceled. - resources: BoundaryResources, + contentState: HoistableState, + fallbackState: HoistableState, trackedContentKeyPath: null | KeyNode, // used to track the path for replay nodes trackedFallbackNode: null | ReplayNode, // used to track the fallback for replay nodes }; @@ -225,10 +224,8 @@ type RenderTask = { childIndex: number, ping: () => void, blockedBoundary: Root | SuspenseBoundary, - blockedBoundaryResources: null | BoundaryResources, // Conceptually part of the blockedBoundary but split out for performance blockedSegment: Segment, // the segment we'll write to - blockedHoistables: Hoistables, // the hoistables we'll write to - parentHoistables: null | Hoistables, + hoistableState: null | HoistableState, // Boundary state we'll mutate while rendering. This may not equal the state of the blockedBoundary abortSet: Set, // the abortable set that this task belongs to keyPath: Root | KeyNode, // the path of all parent keys currently rendering formatContext: FormatContext, // the format's specific context (e.g. HTML/SVG/MathML) @@ -253,10 +250,8 @@ type ReplayTask = { childIndex: number, ping: () => void, blockedBoundary: Root | SuspenseBoundary, - blockedBoundaryResources: null | BoundaryResources, // Conceptually part of the blockedBoundary but split out for performance blockedSegment: null, // we don't write to anything when we replay - blockedHoistables: Hoistables, // contains hoistable state for any child tasks that resume - parentHoistables: null | Hoistables, + hoistableState: null | HoistableState, // Boundary state we'll mutate while rendering. This may not equal the state of the blockedBoundary abortSet: Set, // the abortable set that this task belongs to keyPath: Root | KeyNode, // the path of all parent keys currently rendering formatContext: FormatContext, // the format's specific context (e.g. HTML/SVG/MathML) @@ -294,11 +289,6 @@ type Segment = { textEmbedded: boolean, }; -type Hoistables = { - state: HoistableState, - fallbacks: Set, -}; - const OPEN = 0; const CLOSING = 1; const CLOSED = 2; @@ -308,7 +298,6 @@ export opaque type Request = { flushScheduled: boolean, +resumableState: ResumableState, +renderState: RenderState, - +hoistables: Hoistables, +rootFormatContext: FormatContext, +progressiveChunkSize: number, status: 0 | 1 | 2, @@ -387,13 +376,11 @@ export function createRequest( prepareHostDispatcher(); const pingedTasks: Array = []; const abortSet: Set = new Set(); - const hoistables = createHoistables(); const request: Request = { destination: null, flushScheduled: false, resumableState, renderState, - hoistables, rootFormatContext, progressiveChunkSize: progressiveChunkSize === undefined @@ -438,7 +425,6 @@ export function createRequest( -1, null, rootSegment, - hoistables, null, abortSet, null, @@ -502,13 +488,11 @@ export function resumeRequest( prepareHostDispatcher(); const pingedTasks: Array = []; const abortSet: Set = new Set(); - const hoistables = createHoistables(); const request: Request = { destination: null, flushScheduled: false, resumableState: postponedState.resumableState, renderState, - hoistables, rootFormatContext: postponedState.rootFormatContext, progressiveChunkSize: postponedState.progressiveChunkSize, status: OPEN, @@ -553,7 +537,6 @@ export function resumeRequest( -1, null, rootSegment, - hoistables, null, abortSet, null, @@ -579,7 +562,6 @@ export function resumeRequest( children, -1, null, - hoistables, null, abortSet, null, @@ -616,7 +598,6 @@ function pingTask(request: Request, task: Task): void { function createSuspenseBoundary( request: Request, fallbackAbortableTasks: Set, - fallbackHoistables: null | Hoistables, ): SuspenseBoundary { return { status: PENDING, @@ -624,11 +605,11 @@ function createSuspenseBoundary( parentFlushed: false, pendingTasks: 0, completedSegments: [], - fallbackHoistables, byteSize: 0, fallbackAbortableTasks, errorDigest: null, - resources: createBoundaryResources(), + contentState: createHoistableState(), + fallbackState: createHoistableState(), trackedContentKeyPath: null, trackedFallbackNode: null, }; @@ -641,8 +622,7 @@ function createRenderTask( childIndex: number, blockedBoundary: Root | SuspenseBoundary, blockedSegment: Segment, - blockedHoistables: Hoistables, - parentHoistables: null | Hoistables, + hoistableState: null | HoistableState, abortSet: Set, keyPath: Root | KeyNode, formatContext: FormatContext, @@ -652,13 +632,10 @@ function createRenderTask( componentStack: null | ComponentStackNode, ): RenderTask { request.allPendingTasks++; - let blockedBoundaryResources; if (blockedBoundary === null) { request.pendingRootTasks++; - blockedBoundaryResources = null; } else { blockedBoundary.pendingTasks++; - blockedBoundaryResources = blockedBoundary.resources; } const task: RenderTask = { replay: null, @@ -666,10 +643,8 @@ function createRenderTask( childIndex, ping: () => pingTask(request, task), blockedBoundary, - blockedBoundaryResources, blockedSegment, - blockedHoistables, - parentHoistables, + hoistableState, abortSet, keyPath, formatContext, @@ -690,8 +665,7 @@ function createReplayTask( node: ReactNodeList, childIndex: number, blockedBoundary: Root | SuspenseBoundary, - blockedHoistables: Hoistables, - parentHoistables: null | Hoistables, + hoistableState: null | HoistableState, abortSet: Set, keyPath: Root | KeyNode, formatContext: FormatContext, @@ -701,13 +675,10 @@ function createReplayTask( componentStack: null | ComponentStackNode, ): ReplayTask { request.allPendingTasks++; - let blockedBoundaryResources; if (blockedBoundary === null) { request.pendingRootTasks++; - blockedBoundaryResources = null; } else { blockedBoundary.pendingTasks++; - blockedBoundaryResources = blockedBoundary.resources; } replay.pendingTasks++; const task: ReplayTask = { @@ -716,10 +687,8 @@ function createReplayTask( childIndex, ping: () => pingTask(request, task), blockedBoundary, - blockedBoundaryResources, blockedSegment: null, - blockedHoistables, - parentHoistables, + hoistableState, abortSet, keyPath, formatContext, @@ -755,13 +724,6 @@ function createPendingSegment( }; } -function createHoistables(): Hoistables { - return { - state: createHoistableState(), - fallbacks: new Set(), - }; -} - // DEV-only global reference to the currently executing task let currentTaskInDEV: null | Task = null; function getCurrentStackInDEV(): string { @@ -942,10 +904,8 @@ function renderSuspenseBoundary( const prevKeyPath = task.keyPath; const parentBoundary = task.blockedBoundary; - const parentBoundaryResources = task.blockedBoundaryResources; + const parentHoistableState = task.hoistableState; const parentSegment = task.blockedSegment; - const parentHoistables = task.blockedHoistables; - const grandParentHoistables = task.parentHoistables; // Each time we enter a suspense boundary, we split out into a new segment for // the fallback so that we can later replace that segment with the content. @@ -955,13 +915,7 @@ function renderSuspenseBoundary( const content: ReactNodeList = props.children; const fallbackAbortSet: Set = new Set(); - const fallbackHoistables = createHoistables(); - parentHoistables.fallbacks.add(fallbackHoistables); - const newBoundary = createSuspenseBoundary( - request, - fallbackAbortSet, - fallbackHoistables, - ); + const newBoundary = createSuspenseBoundary(request, fallbackAbortSet); if (request.trackedPostpones !== null) { newBoundary.trackedContentKeyPath = keyPath; } @@ -1003,10 +957,8 @@ function renderSuspenseBoundary( // context switching. We just need to temporarily switch which boundary and which segment // we're writing to. If something suspends, it'll spawn new suspended task with that context. task.blockedBoundary = newBoundary; - task.blockedBoundaryResources = newBoundary.resources; + task.hoistableState = newBoundary.contentState; task.blockedSegment = contentRootSegment; - task.blockedHoistables = createHoistables(); - task.parentHoistables = parentHoistables; task.keyPath = keyPath; try { @@ -1028,7 +980,6 @@ function renderSuspenseBoundary( // We are returning early so we need to restore the task.componentStack = previousComponentStack; - mergeHoistables.call(parentHoistables, task.blockedHoistables); return; } } catch (error: mixed) { @@ -1058,10 +1009,8 @@ function renderSuspenseBoundary( // We do need to fallthrough to create the fallback though. } finally { task.blockedBoundary = parentBoundary; - task.blockedBoundaryResources = parentBoundaryResources; + task.hoistableState = parentHoistableState; task.blockedSegment = parentSegment; - task.blockedHoistables = parentHoistables; - task.parentHoistables = grandParentHoistables; task.keyPath = prevKeyPath; task.componentStack = previousComponentStack; } @@ -1088,7 +1037,6 @@ function renderSuspenseBoundary( newBoundary.trackedFallbackNode = fallbackReplayNode; } } - // We create suspended task for the fallback because we don't want to actually work // on it yet in case we finish the main content, so we queue for later. const suspendedFallbackTask = createRenderTask( @@ -1098,8 +1046,7 @@ function renderSuspenseBoundary( -1, parentBoundary, boundarySegment, - fallbackHoistables, - null, + newBoundary.fallbackState, fallbackAbortSet, fallbackKeyPath, task.formatContext, @@ -1136,21 +1083,13 @@ function replaySuspenseBoundary( const previousReplaySet: ReplaySet = task.replay; const parentBoundary = task.blockedBoundary; - const parentBoundaryResources = task.blockedBoundaryResources; - const parentHoistables = task.blockedHoistables; - const grandParentHoistables = task.parentHoistables; + const parentHoistableState = task.hoistableState; const content: ReactNodeList = props.children; const fallback: ReactNodeList = props.fallback; const fallbackAbortSet: Set = new Set(); - const fallbackHoistables = createHoistables(); - parentHoistables.fallbacks.add(fallbackHoistables); - const resumedBoundary = createSuspenseBoundary( - request, - fallbackAbortSet, - fallbackHoistables, - ); + const resumedBoundary = createSuspenseBoundary(request, fallbackAbortSet); resumedBoundary.parentFlushed = true; // We restore the same id of this boundary as was used during prerender. resumedBoundary.rootSegmentID = id; @@ -1159,10 +1098,9 @@ function replaySuspenseBoundary( // context switching. We just need to temporarily switch which boundary and replay node // we're writing to. If something suspends, it'll spawn new suspended task with that context. task.blockedBoundary = resumedBoundary; - task.blockedBoundaryResources = resumedBoundary.resources; - task.blockedHoistables = createHoistables(); - task.parentHoistables = parentHoistables; + task.hoistableState = resumedBoundary.contentState; task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1}; + try { // We use the safe form because we don't handle suspending here. Only error handling. renderNode(request, task, content, -1); @@ -1217,9 +1155,7 @@ function replaySuspenseBoundary( // We do need to fallthrough to create the fallback though. } finally { task.blockedBoundary = parentBoundary; - task.blockedBoundaryResources = parentBoundaryResources; - task.blockedHoistables = parentHoistables; - task.parentHoistables = grandParentHoistables; + task.hoistableState = parentHoistableState; task.replay = previousReplaySet; task.keyPath = prevKeyPath; task.componentStack = previousComponentStack; @@ -1241,8 +1177,7 @@ function replaySuspenseBoundary( fallback, -1, parentBoundary, - fallbackHoistables, - null, + resumedBoundary.fallbackState, fallbackAbortSet, fallbackKeyPath, task.formatContext, @@ -1312,15 +1247,13 @@ function renderHostElement( task.keyPath = prevKeyPath; } else { // Render - const hoistables = task.blockedHoistables; const children = pushStartInstance( segment.chunks, type, props, request.resumableState, request.renderState, - task.blockedBoundaryResources, - hoistables.state, + task.hoistableState, task.formatContext, segment.lastPushedText, ); @@ -2805,8 +2738,7 @@ function spawnNewSuspendedReplayTask( task.node, task.childIndex, task.blockedBoundary, - task.blockedHoistables, - task.parentHoistables, + task.hoistableState, task.abortSet, task.keyPath, task.formatContext, @@ -2851,8 +2783,7 @@ function spawnNewSuspendedRenderTask( task.childIndex, task.blockedBoundary, newSegment, - task.blockedHoistables, - task.parentHoistables, + task.hoistableState, task.abortSet, task.keyPath, task.formatContext, @@ -3130,13 +3061,11 @@ function abortTaskSoft(this: Request, task: Task): void { // It's used for when we didn't need this task to complete the tree. // If task was needed, then it should use abortTask instead. const request: Request = this; + const boundary = task.blockedBoundary; const segment = task.blockedSegment; if (segment !== null) { - const boundary = task.blockedBoundary; - const hoistables = task.blockedHoistables; - const parentHoistables = task.parentHoistables; segment.status = ABORTED; - finishedTask(request, boundary, segment, hoistables, parentHoistables); + finishedTask(request, boundary, segment); } } @@ -3147,7 +3076,7 @@ function abortRemainingSuspenseBoundary( errorDigest: ?string, errorInfo: ThrownInfo, ): void { - const resumedBoundary = createSuspenseBoundary(request, new Set(), null); + const resumedBoundary = createSuspenseBoundary(request, new Set()); resumedBoundary.parentFlushed = true; // We restore the same id of this boundary as was used during prerender. resumedBoundary.rootSegmentID = rootSegmentID; @@ -3395,31 +3324,10 @@ function queueCompletedSegment( } } -// Merges the internal state of Hoistables from a source to a target. afterwards -// both source and target will share the same unified state. This technique is used -// so we can resolve tasks in any order and always accumulate up to the root Hoistable. -// This function uses the `this` argument to allow for slightly optimized calling from -// a forEach over a Set. -function mergeHoistables(this: Hoistables, source: Hoistables) { - if (enableFloat) { - const target = this; - hoistHoistables(target.state, source.state); - source.fallbacks.forEach(h => { - target.fallbacks.add(h); - }); - // we assign the parent state and fallbacks to the child state so if a child task completes - // it will hoist and merge with the parent Hoistables - source.state = target.state; - source.fallbacks = target.fallbacks; - } -} - function finishedTask( request: Request, boundary: Root | SuspenseBoundary, segment: null | Segment, - hoistables: Hoistables, - parentHoistables: null | Hoistables, ) { if (boundary === null) { if (segment !== null && segment.parentFlushed) { @@ -3458,20 +3366,6 @@ function finishedTask( request.completedBoundaries.push(boundary); } - if (enableFloat && parentHoistables) { - // We have completed a boundary and need to merge this boundary's Hoistables with the parent - // Hoistables for this task. First we remove the fallback Hoistables. If they already flushed - // we can't do anythign about it but we don't want to flush them now if unflushed because the fallback - // will never show - if (boundary.fallbackHoistables) { - parentHoistables.fallbacks.delete(boundary.fallbackHoistables); - } - // Next we merge the boundary Hoistables into the task Hoistables. In the process the boundary assumes - // the task Hoistables internal state so later if a child task also completes it will merge with - // the appropriate sets - mergeHoistables.call(parentHoistables, hoistables); - } - // We can now cancel any pending task on the fallback since we won't need to show it anymore. // This needs to happen after we read the parentFlushed flags because aborting can finish // work which can trigger user code, which can start flushing, which can change those flags. @@ -3572,13 +3466,7 @@ function retryRenderTask( task.abortSet.delete(task); segment.status = COMPLETED; - finishedTask( - request, - task.blockedBoundary, - segment, - task.blockedHoistables, - task.parentHoistables, - ); + finishedTask(request, task.blockedBoundary, segment); } catch (thrownValue) { resetHooksState(); @@ -3619,13 +3507,7 @@ function retryRenderTask( const postponeInfo = getThrownInfo(request, task.componentStack); logPostpone(request, postponeInstance.message, postponeInfo); trackPostpone(request, trackedPostpones, task, segment); - finishedTask( - request, - task.blockedBoundary, - segment, - task.blockedHoistables, - task.parentHoistables, - ); + finishedTask(request, task.blockedBoundary, segment); return; } } @@ -3685,13 +3567,7 @@ function retryReplayTask(request: Request, task: ReplayTask): void { task.replay.pendingTasks--; task.abortSet.delete(task); - finishedTask( - request, - task.blockedBoundary, - null, - task.blockedHoistables, - task.parentHoistables, - ); + finishedTask(request, task.blockedBoundary, null); } catch (thrownValue) { resetHooksState(); @@ -3804,11 +3680,66 @@ export function performWork(request: Request): void { } } +function preparePreambleForSubtree(request: Request, segment: Segment): void { + if (segment.status === COMPLETED) { + const children = segment.children; + for (let i = 0; i < children.length; i++) { + const child = children[i]; + prepareSegmentForPreamble(request, child); + } + } +} + +function prepareSegmentForPreamble(request: Request, segment: Segment): void { + const boundary = segment.boundary; + if (boundary === null) { + // Not a suspense boundary. + preparePreambleForSubtree(request, segment); + } else { + if (boundary.status === COMPLETED) { + // we are going to flush this boundary's primary content rather than a fallback + hoistToRoot(request.renderState, boundary.contentState); + // Traverse down the primary content path. + const completedSegments = boundary.completedSegments; + const contentSegment = completedSegments[0]; + if (contentSegment) { + // It is an invariant that a previously unvisited boundary have a single root + // segment however we know this will be caught in the normal flushing path + // so we simply guard the condition here and avoid throwing + preparePreambleForSubtree(request, segment); + } + } else { + // We are going to flush this boundary's fallback content and should include + // it's hoistables as well + hoistToRoot(request.renderState, boundary.fallbackState); + // Traverse the fallback path and see if there are any deeper boundaries with hoistables + // to collect + preparePreambleForSubtree(request, segment); + } + } +} + +function flushPreamble( + request: Request, + destination: Destination, + rootSegment: Segment, +) { + prepareSegmentForPreamble(request, rootSegment); + const willFlushAllSegments = + request.allPendingTasks === 0 && request.trackedPostpones === null; + writePreamble( + destination, + request.resumableState, + request.renderState, + willFlushAllSegments, + ); +} + function flushSubtree( request: Request, destination: Destination, segment: Segment, - rootBoundary: null | SuspenseBoundary, + hoistableState: null | HoistableState, ): boolean { segment.parentFlushed = true; switch (segment.status) { @@ -3838,7 +3769,7 @@ function flushSubtree( for (; chunkIdx < nextChild.index; chunkIdx++) { writeChunk(destination, chunks[chunkIdx]); } - r = flushSegment(request, destination, nextChild, rootBoundary); + r = flushSegment(request, destination, nextChild, hoistableState); } // Finally just write all the remaining chunks for (; chunkIdx < chunks.length - 1; chunkIdx++) { @@ -3861,12 +3792,12 @@ function flushSegment( request: Request, destination: Destination, segment: Segment, - rootBoundary: null | SuspenseBoundary, + hoistableState: null | HoistableState, ): boolean { const boundary = segment.boundary; if (boundary === null) { // Not a suspense boundary. - return flushSubtree(request, destination, segment, rootBoundary); + return flushSubtree(request, destination, segment, hoistableState); } boundary.parentFlushed = true; @@ -3884,7 +3815,7 @@ function flushSegment( boundary.errorComponentStack, ); // Flush the fallback. - flushSubtree(request, destination, segment, rootBoundary); + flushSubtree(request, destination, segment, hoistableState); return writeEndClientRenderedSuspenseBoundary( destination, @@ -3907,8 +3838,15 @@ function flushSegment( const id = boundary.rootSegmentID; writeStartPendingSuspenseBoundary(destination, request.renderState, id); + // We are going to flush the fallback so we need to hoist the fallback + // state to the parent boundary + if (enableFloat) { + if (hoistableState) { + hoistToBoundary(hoistableState, boundary.fallbackState); + } + } // Flush the fallback. - flushSubtree(request, destination, segment, rootBoundary); + flushSubtree(request, destination, segment, hoistableState); return writeEndPendingSuspenseBoundary(destination, request.renderState); } else if (boundary.byteSize > request.progressiveChunkSize) { @@ -3929,13 +3867,20 @@ function flushSegment( boundary.rootSegmentID, ); + // While we are going to flush the fallback we are going to follow it up with + // the completed boundary immediately so we make the choice to omit fallback + // boundary state from the parent since it will be replaced when the boundary + // flushes later in this pass or in a future flush + // Flush the fallback. - flushSubtree(request, destination, segment, rootBoundary); + flushSubtree(request, destination, segment, hoistableState); return writeEndPendingSuspenseBoundary(destination, request.renderState); } else { - if (enableFloat && rootBoundary && rootBoundary !== boundary) { - hoistBoundaryResources(rootBoundary.resources, boundary.resources); + if (enableFloat) { + if (hoistableState) { + hoistToBoundary(hoistableState, boundary.contentState); + } } // We can inline this boundary's content as a complete boundary. writeStartCompletedSuspenseBoundary(destination, request.renderState); @@ -3949,7 +3894,7 @@ function flushSegment( } const contentSegment = completedSegments[0]; - flushSegment(request, destination, contentSegment, rootBoundary); + flushSegment(request, destination, contentSegment, hoistableState); return writeEndCompletedSuspenseBoundary(destination, request.renderState); } @@ -3975,7 +3920,7 @@ function flushSegmentContainer( request: Request, destination: Destination, segment: Segment, - boundary: SuspenseBoundary, + hoistableState: HoistableState, ): boolean { writeStartSegment( destination, @@ -3983,7 +3928,7 @@ function flushSegmentContainer( segment.parentFormatContext, segment.id, ); - flushSegment(request, destination, segment, boundary); + flushSegment(request, destination, segment, hoistableState); return writeEndSegment(destination, segment.parentFormatContext); } @@ -4001,9 +3946,9 @@ function flushCompletedBoundary( completedSegments.length = 0; if (enableFloat) { - writeResourcesForBoundary( + writeHoistablesForCompletedBoundary( destination, - boundary.resources, + boundary.contentState, request.renderState, ); } @@ -4013,7 +3958,7 @@ function flushCompletedBoundary( request.resumableState, request.renderState, boundary.rootSegmentID, - boundary.resources, + boundary.contentState, ); } @@ -4039,13 +3984,9 @@ function flushPartialBoundary( completedSegments.splice(0, i); if (enableFloat) { - // The way this is structured we only write resources for partial boundaries - // if there is no backpressure. Later before we complete the boundary we - // will write resources regardless of backpressure before we emit the - // completion instruction - return writeResourcesForBoundary( + return writeHoistablesForPartialBoundary( destination, - boundary.resources, + boundary.contentState, request.renderState, ); } else { @@ -4064,6 +4005,8 @@ function flushPartiallyCompletedSegment( return true; } + const hoistableState = boundary.contentState; + const segmentID = segment.id; if (segmentID === -1) { // This segment wasn't previously referred to. This happens at the root of @@ -4076,13 +4019,13 @@ function flushPartiallyCompletedSegment( ); } - return flushSegmentContainer(request, destination, segment, boundary); + return flushSegmentContainer(request, destination, segment, hoistableState); } else if (segmentID === boundary.rootSegmentID) { // When we emit postponed boundaries, we might have assigned the ID already // but it's still the root segment so we can't inject it into the parent yet. - return flushSegmentContainer(request, destination, segment, boundary); + return flushSegmentContainer(request, destination, segment, hoistableState); } else { - flushSegmentContainer(request, destination, segment, boundary); + flushSegmentContainer(request, destination, segment, hoistableState); return writeCompletedSegmentInstruction( destination, request.resumableState, @@ -4092,17 +4035,6 @@ function flushPartiallyCompletedSegment( } } -function prepareToFlushHoistables(request: Request) { - // At the moment we flush we merge all fallback Hoistables visible to the request's Hoistables - // object. These represent hoistables for Boundaries that will flush a fallback because the - // primary content isn't ready yet. If a boundary completes before this step then the fallback - // Hoistables would have already been removed from this set so we know it only includes necessary - // fallback hoistables - const requestHoistables = request.hoistables; - requestHoistables.fallbacks.forEach(mergeHoistables, requestHoistables); - requestHoistables.fallbacks.clear(); -} - function flushCompletedQueues( request: Request, destination: Destination, @@ -4122,14 +4054,7 @@ function flushCompletedQueues( return; } else if (request.pendingRootTasks === 0) { if (enableFloat) { - prepareToFlushHoistables(request); - writePreamble( - destination, - request.resumableState, - request.renderState, - request.hoistables.state, - request.allPendingTasks === 0 && request.trackedPostpones === null, - ); + flushPreamble(request, destination, completedRootSegment); } flushSegment(request, destination, completedRootSegment, null); @@ -4140,15 +4065,8 @@ function flushCompletedQueues( return; } } - if (enableFloat) { - prepareToFlushHoistables(request); - writeHoistables( - destination, - request.resumableState, - request.renderState, - request.hoistables.state, - ); + writeHoistables(destination, request.resumableState, request.renderState); } // We emit client rendering instructions for already emitted boundaries first. diff --git a/packages/react-server/src/forks/ReactFizzConfig.custom.js b/packages/react-server/src/forks/ReactFizzConfig.custom.js index 7a2c256fc025f..e113500a4bf54 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.custom.js +++ b/packages/react-server/src/forks/ReactFizzConfig.custom.js @@ -31,7 +31,6 @@ export opaque type Destination = mixed; // eslint-disable-line no-undef export opaque type RenderState = mixed; export opaque type HoistableState = mixed; export opaque type ResumableState = mixed; -export opaque type BoundaryResources = mixed; export opaque type FormatContext = mixed; export opaque type HeadersDescriptor = mixed; export type {TransitionStatus}; @@ -88,9 +87,11 @@ export const NotPendingTransition = $$$config.NotPendingTransition; export const writePreamble = $$$config.writePreamble; export const writeHoistables = $$$config.writeHoistables; export const writePostamble = $$$config.writePostamble; -export const hoistBoundaryResources = $$$config.hoistBoundaryResources; -export const hoistHoistables = $$$config.hoistHoistables; +export const hoistToBoundary = $$$config.hoistToBoundary; +export const hoistToRoot = $$$config.hoistToRoot; export const createHoistableState = $$$config.createHoistableState; -export const createBoundaryResources = $$$config.createBoundaryResources; -export const writeResourcesForBoundary = $$$config.writeResourcesForBoundary; +export const writeHoistablesForPartialBoundary = + $$$config.writeHoistablesForPartialBoundary; +export const writeHoistablesForCompletedBoundary = + $$$config.writeHoistablesForCompletedBoundary; export const emitEarlyPreloads = $$$config.emitEarlyPreloads;