@samitouri / QOS-React-2 / commits / 554fc49f41

[Fizz] improve Hoistable handling for Elements and Resources inside Suspense Boundaries (#28069)

Updates Fizz to handle Hoistables (Resources and Elements) in a way that better aligns with Suspense fallbacks 1. Hoistable Elements inside a fallback (regardless of how deep and how many additional boundaries are intermediate) will be ignored. The reasoning is fallbacks are transient and since there is not good way to clean up hoistables because they escape their Suspense container its better to not emit them in the first place. SSR fallbacks are already not full fidelity because they never hydrate so this aligns with that somewhat. 2. Hoistable stylesheets in fallbacks will only block the reveal of a parent suspense boundary if the fallback is going to flush with that completed parent suspense boundary. Previously if you rendered a stylesheet Resource inside a fallback any parent suspense boundaries that completed after the shell flushed would include that resource in the set required to resolve before the boundary reveal happens on the client. This is not a semantic change, just a performance optimization 3. preconnect and preload hoistable queues are gone, if you want to optimize resource loading you shoudl use `ReactDOM.preconnect` and `ReactDOM.preload`. `viewport` meta tags get their own queue because they need to go before any preloads since they affect the media state. In addition to those functional changes this PR also refactors the boundary resource tracking by moving it to the task rather than using function calls at the start of each render and flush. Tasks also now track whether they are a fallback task supercedes prior work here: https://github.com/facebook/react/pull/27534

Josh Story committed Jan 30, 2024 at 10:14 UTC 554fc49f41465d914b15dc8eb2ec094f37824f7e
6 files changed +488 -316
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+94 -93
@@ -147,12 +147,11 @@ export type RenderState = {
147 // external runtime script chunks
148 externalRuntimeScript: null | ExternalRuntimeScript,
149 bootstrapChunks: Array<Chunk | PrecomputedChunk>,
150 + importMapChunks: Array<Chunk | PrecomputedChunk>,
151
152 // Hoistable chunks
153 charsetChunks: Array<Chunk | PrecomputedChunk>,
153 - preconnectChunks: Array<Chunk | PrecomputedChunk>,
154 - importMapChunks: Array<Chunk | PrecomputedChunk>,
155 - preloadChunks: Array<Chunk | PrecomputedChunk>,
154 + viewportChunks: Array<Chunk | PrecomputedChunk>,
155 hoistableChunks: Array<Chunk | PrecomputedChunk>,
156
157 // Headers queues for Resources that can flush early
@@ -201,9 +200,6 @@ export type RenderState = {
200 moduleScripts: Map<string, Resource>,
201 },
202
204 - // Module-global-like reference for current boundary resources
205 - boundaryResources: ?BoundaryResources,
206 -
203 // Module-global-like reference for flushing/hoisting state of style resources
204 // We need to track whether the current request has flushed any style resources
205 // without sending an instruction to hoist them. we do that here
@@ -457,6 +453,7 @@ export function createRenderState(
453
454 externalRuntimeScript: externalRuntimeScript,
455 bootstrapChunks: bootstrapChunks,
456 + importMapChunks,
457
458 onHeaders,
459 headers,
@@ -473,9 +470,7 @@ export function createRenderState(
470 },
471
472 charsetChunks: [],
476 - preconnectChunks: [],
477 - importMapChunks,
478 - preloadChunks: [],
473 + viewportChunks: [],
474 hoistableChunks: [],
475
476 // cleared on flush
@@ -497,7 +492,7 @@ export function createRenderState(
492
493 nonce,
494 // like a module global for currently rendering boundary
500 - boundaryResources: null,
495 + hoistableState: null,
496 stylesToHoist: false,
497 };
498
@@ -2230,6 +2225,7 @@ function pushMeta(
2225 textEmbedded: boolean,
2226 insertionMode: InsertionMode,
2227 noscriptTagInScope: boolean,
2228 + isFallback: boolean,
2229 ): null {
2230 if (enableFloat) {
2231 if (
@@ -2245,11 +2241,24 @@ function pushMeta(
2241 target.push(textSeparator);
2242 }
2243
2248 - if (typeof props.charSet === 'string') {
2244 + if (isFallback) {
2245 + // Hoistable Elements for fallbacks are simply omitted. we don't want to emit them early
2246 + // because they are likely superceded by primary content and we want to avoid needing to clean
2247 + // them up when the primary content is ready. They are never hydrated on the client anyway because
2248 + // boundaries in fallback are awaited or client render, in either case there is never hydration
2249 + return null;
2250 + } else if (typeof props.charSet === 'string') {
2251 + // "charset" Should really be config and not picked up from tags however since this is
2252 + // the only way to embed the tag today we flush it on a special queue on the Request so it
2253 + // can go before everything else. Like viewport this means that the tag will escape it's
2254 + // parent container.
2255 return pushSelfClosing(renderState.charsetChunks, props, 'meta');
2256 } else if (props.name === 'viewport') {
2251 - // "viewport" isn't related to preconnect but it has the right priority
2252 - return pushSelfClosing(renderState.preconnectChunks, props, 'meta');
2257 + // "viewport" is flushed on the Request so it can go earlier that Float resources that
2258 + // might be affected by it. This means it can escape the boundary it is rendered within.
2259 + // This is a pragmatic solution to viewport being incredibly sensitive to document order
2260 + // without requiring all hoistables to be flushed too early.
2261 + return pushSelfClosing(renderState.viewportChunks, props, 'meta');
2262 } else {
2263 return pushSelfClosing(renderState.hoistableChunks, props, 'meta');
2264 }
@@ -2264,9 +2273,11 @@ function pushLink(
2273 props: Object,
2274 resumableState: ResumableState,
2275 renderState: RenderState,
2276 + hoistableState: null | HoistableState,
2277 textEmbedded: boolean,
2278 insertionMode: InsertionMode,
2279 noscriptTagInScope: boolean,
2280 + isFallback: boolean,
2281 ): null {
2282 if (enableFloat) {
2283 const rel = props.rel;
@@ -2384,8 +2395,8 @@ function pushLink(
2395 // We add the newly created resource to our StyleQueue and if necessary
2396 // track the resource with the currently rendering boundary
2397 styleQueue.sheets.set(key, resource);
2387 - if (renderState.boundaryResources) {
2388 - renderState.boundaryResources.stylesheets.add(resource);
2398 + if (hoistableState) {
2399 + hoistableState.stylesheets.add(resource);
2400 }
2401 } else {
2402 // We need to track whether this boundary should wait on this resource or not.
@@ -2396,8 +2407,8 @@ function pushLink(
2407 if (styleQueue) {
2408 const resource = styleQueue.sheets.get(key);
2409 if (resource) {
2399 - if (renderState.boundaryResources) {
2400 - renderState.boundaryResources.stylesheets.add(resource);
2410 + if (hoistableState) {
2411 + hoistableState.stylesheets.add(resource);
2412 }
2413 }
2414 }
@@ -2422,14 +2433,14 @@ function pushLink(
2433 target.push(textSeparator);
2434 }
2435
2425 - switch (props.rel) {
2426 - case 'preconnect':
2427 - case 'dns-prefetch':
2428 - return pushLinkImpl(renderState.preconnectChunks, props);
2429 - case 'preload':
2430 - return pushLinkImpl(renderState.preloadChunks, props);
2431 - default:
2432 - return pushLinkImpl(renderState.hoistableChunks, props);
2436 + if (isFallback) {
2437 + // Hoistable Elements for fallbacks are simply omitted. we don't want to emit them early
2438 + // because they are likely superceded by primary content and we want to avoid needing to clean
2439 + // them up when the primary content is ready. They are never hydrated on the client anyway because
2440 + // boundaries in fallback are awaited or client render, in either case there is never hydration
2441 + return null;
2442 + } else {
2443 + return pushLinkImpl(renderState.hoistableChunks, props);
2444 }
2445 }
2446 } else {
@@ -2472,6 +2483,7 @@ function pushStyle(
2483 props: Object,
2484 resumableState: ResumableState,
2485 renderState: RenderState,
2486 + hoistableState: null | HoistableState,
2487 textEmbedded: boolean,
2488 insertionMode: InsertionMode,
2489 noscriptTagInScope: boolean,
@@ -2571,8 +2583,8 @@ function pushStyle(
2583 // it. However, it's possible when you resume that the style has already been emitted
2584 // and then it wouldn't be recreated in the RenderState and there's no need to track
2585 // it again since we should've hoisted it to the shell already.
2574 - if (renderState.boundaryResources) {
2575 - renderState.boundaryResources.styles.add(styleQueue);
2586 + if (hoistableState) {
2587 + hoistableState.styles.add(styleQueue);
2588 }
2589 }
2590
@@ -2885,6 +2897,7 @@ function pushTitle(
2897 renderState: RenderState,
2898 insertionMode: InsertionMode,
2899 noscriptTagInScope: boolean,
2900 + isFallback: boolean,
2901 ): ReactNodeList {
2902 if (__DEV__) {
2903 if (hasOwnProperty.call(props, 'children')) {
@@ -2940,8 +2953,15 @@ function pushTitle(
2953 !noscriptTagInScope &&
2954 props.itemProp == null
2955 ) {
2943 - pushTitleImpl(renderState.hoistableChunks, props);
2944 - return null;
2956 + if (isFallback) {
2957 + // Hoistable Elements for fallbacks are simply omitted. we don't want to emit them early
2958 + // because they are likely superceded by primary content and we want to avoid needing to clean
2959 + // them up when the primary content is ready. They are never hydrated on the client anyway because
2960 + // boundaries in fallback are awaited or client render, in either case there is never hydration
2961 + return null;
2962 + } else {
2963 + pushTitleImpl(renderState.hoistableChunks, props);
2964 + }
2965 } else {
2966 return pushTitleImpl(target, props);
2967 }
@@ -3472,8 +3492,10 @@ export function pushStartInstance(
3492 props: Object,
3493 resumableState: ResumableState,
3494 renderState: RenderState,
3495 + hoistableState: null | HoistableState,
3496 formatContext: FormatContext,
3497 textEmbedded: boolean,
3498 + isFallback: boolean,
3499 ): ReactNodeList {
3500 if (__DEV__) {
3501 validateARIAProperties(type, props);
@@ -3542,6 +3564,7 @@ export function pushStartInstance(
3564 renderState,
3565 formatContext.insertionMode,
3566 !!(formatContext.tagScope & NOSCRIPT_SCOPE),
3567 + isFallback,
3568 )
3569 : pushStartTitle(target, props);
3570 case 'link':
@@ -3550,9 +3573,11 @@ export function pushStartInstance(
3573 props,
3574 resumableState,
3575 renderState,
3576 + hoistableState,
3577 textEmbedded,
3578 formatContext.insertionMode,
3579 !!(formatContext.tagScope & NOSCRIPT_SCOPE),
3580 + isFallback,
3581 );
3582 case 'script':
3583 return enableFloat
@@ -3572,6 +3597,7 @@ export function pushStartInstance(
3597 props,
3598 resumableState,
3599 renderState,
3600 + hoistableState,
3601 textEmbedded,
3602 formatContext.insertionMode,
3603 !!(formatContext.tagScope & NOSCRIPT_SCOPE),
@@ -3584,6 +3610,7 @@ export function pushStartInstance(
3610 textEmbedded,
3611 formatContext.insertionMode,
3612 !!(formatContext.tagScope & NOSCRIPT_SCOPE),
3613 + isFallback,
3614 );
3615 // Newline eating tags
3616 case 'listing':
@@ -4120,7 +4147,7 @@ export function writeCompletedBoundaryInstruction(
4147 resumableState: ResumableState,
4148 renderState: RenderState,
4149 id: number,
4123 - boundaryResources: BoundaryResources,
4150 + hoistableState: HoistableState,
4151 ): boolean {
4152 let requiresStyleInsertion;
4153 if (enableFloat) {
@@ -4196,11 +4223,11 @@ export function writeCompletedBoundaryInstruction(
4223 // e.g. [&#34;A&#34;, &#34;B&#34;]
4224 if (scriptFormat) {
4225 writeChunk(destination, completeBoundaryScript3a);
4199 - // boundaryResources encodes an array literal
4200 - writeStyleResourceDependenciesInJS(destination, boundaryResources);
4226 + // hoistableState encodes an array literal
4227 + writeStyleResourceDependenciesInJS(destination, hoistableState);
4228 } else {
4229 writeChunk(destination, completeBoundaryData3a);
4203 - writeStyleResourceDependenciesInAttr(destination, boundaryResources);
4230 + writeStyleResourceDependenciesInAttr(destination, hoistableState);
4231 }
4232 } else {
4233 if (scriptFormat) {
@@ -4449,9 +4476,9 @@ function hasStylesToHoist(stylesheet: StylesheetResource): boolean {
4476 return false;
4477 }
4478
4452 -export function writeResourcesForBoundary(
4479 +export function writeHoistablesForBoundary(
4480 destination: Destination,
4454 - boundaryResources: BoundaryResources,
4481 + hoistableState: HoistableState,
4482 renderState: RenderState,
4483 ): boolean {
4484 // Reset these on each invocation, they are only safe to read in this function
@@ -4459,10 +4486,15 @@ export function writeResourcesForBoundary(
4486 destinationHasCapacity = true;
4487
4488 // Flush style tags for each precedence this boundary depends on
4462 - boundaryResources.styles.forEach(flushStyleTagsLateForBoundary, destination);
4489 + hoistableState.styles.forEach(flushStyleTagsLateForBoundary, destination);
4490
4491 // Determine if this boundary has stylesheets that need to be awaited upon completion
4465 - boundaryResources.stylesheets.forEach(hasStylesToHoist);
4492 + hoistableState.stylesheets.forEach(hasStylesToHoist);
4493 +
4494 + // We don't actually want to flush any hoistables until the boundary is complete so we omit
4495 + // any further writing here. This is becuase unlike Resources, Hoistable Elements act more like
4496 + // regular elements, each rendered element has a unique representation in the DOM. We don't want
4497 + // these elements to appear in the DOM early, before the boundary has actually completed
4498
4499 if (currentlyRenderingBoundaryHasStylesToHoist) {
4500 renderState.stylesToHoist = true;
@@ -4629,11 +4661,11 @@ export function writePreamble(
4661 renderState.preconnects.forEach(flushResource, destination);
4662 renderState.preconnects.clear();
4663
4632 - const preconnectChunks = renderState.preconnectChunks;
4633 - for (i = 0; i < preconnectChunks.length; i++) {
4634 - writeChunk(destination, preconnectChunks[i]);
4664 + const viewportChunks = renderState.viewportChunks;
4665 + for (i = 0; i < viewportChunks.length; i++) {
4666 + writeChunk(destination, viewportChunks[i]);
4667 }
4636 - preconnectChunks.length = 0;
4668 + viewportChunks.length = 0;
4669
4670 renderState.fontPreloads.forEach(flushResource, destination);
4671 renderState.fontPreloads.clear();
@@ -4658,13 +4690,6 @@ export function writePreamble(
4690 renderState.bulkPreloads.forEach(flushResource, destination);
4691 renderState.bulkPreloads.clear();
4692
4661 - // Write embedding preloadChunks
4662 - const preloadChunks = renderState.preloadChunks;
4663 - for (i = 0; i < preloadChunks.length; i++) {
4664 - writeChunk(destination, preloadChunks[i]);
4665 - }
4666 - preloadChunks.length = 0;
4667 -
4693 // Write embedding hoistableChunks
4694 const hoistableChunks = renderState.hoistableChunks;
4695 for (i = 0; i < hoistableChunks.length; i++) {
@@ -4672,13 +4697,9 @@ export function writePreamble(
4697 }
4698 hoistableChunks.length = 0;
4699
4675 - // Flush closing head if necessary
4700 if (htmlChunks && headChunks === null) {
4677 - // We have an <html> rendered but no <head> rendered. We however inserted
4678 - // a <head> up above so we need to emit the </head> now. This is safe because
4679 - // if the main content contained the </head> it would also have provided a
4680 - // <head>. This means that all the content inside <html> is either <body> or
4681 - // invalid HTML
4701 + // we have an <html> but we inserted an implicit <head> tag. We need
4702 + // to close it since the main content won't have it
4703 writeChunk(destination, endChunkForTag('head'));
4704 }
4705 }
@@ -4699,15 +4720,15 @@ export function writeHoistables(
4720 // We omit charsetChunks because we have already sent the shell and if it wasn't
4721 // already sent it is too late now.
4722
4723 + const viewportChunks = renderState.viewportChunks;
4724 + for (i = 0; i < viewportChunks.length; i++) {
4725 + writeChunk(destination, viewportChunks[i]);
4726 + }
4727 + viewportChunks.length = 0;
4728 +
4729 renderState.preconnects.forEach(flushResource, destination);
4730 renderState.preconnects.clear();
4731
4705 - const preconnectChunks = renderState.preconnectChunks;
4706 - for (i = 0; i < preconnectChunks.length; i++) {
4707 - writeChunk(destination, preconnectChunks[i]);
4708 - }
4709 - preconnectChunks.length = 0;
4710 -
4732 renderState.fontPreloads.forEach(flushResource, destination);
4733 renderState.fontPreloads.clear();
4734
@@ -4732,13 +4753,6 @@ export function writeHoistables(
4753 renderState.bulkPreloads.forEach(flushResource, destination);
4754 renderState.bulkPreloads.clear();
4755
4735 - // Write embedding preloadChunks
4736 - const preloadChunks = renderState.preloadChunks;
4737 - for (i = 0; i < preloadChunks.length; i++) {
4738 - writeChunk(destination, preloadChunks[i]);
4739 - }
4740 - preloadChunks.length = 0;
4741 -
4756 // Write embedding hoistableChunks
4757 const hoistableChunks = renderState.hoistableChunks;
4758 for (i = 0; i < hoistableChunks.length; i++) {
@@ -4769,12 +4783,12 @@ const arrayCloseBracket = stringToPrecomputedChunk(']');
4783 // [["JS_escaped_string1", "JS_escaped_string2"]]
4784 function writeStyleResourceDependenciesInJS(
4785 destination: Destination,
4772 - boundaryResources: BoundaryResources,
4786 + hoistableState: HoistableState,
4787 ): void {
4788 writeChunk(destination, arrayFirstOpenBracket);
4789
4790 let nextArrayOpenBrackChunk = arrayFirstOpenBracket;
4777 - boundaryResources.stylesheets.forEach(resource => {
4791 + hoistableState.stylesheets.forEach(resource => {
4792 if (resource.state === PREAMBLE) {
4793 // We can elide this dependency because it was flushed in the shell and
4794 // should be ready before content is shown on the client
@@ -4962,12 +4976,12 @@ function writeStyleResourceAttributeInJS(
4976 // [[&quot;JSON_escaped_string1&quot;, &quot;JSON_escaped_string2&quot;]]
4977 function writeStyleResourceDependenciesInAttr(
4978 destination: Destination,
4965 - boundaryResources: BoundaryResources,
4979 + hoistableState: HoistableState,
4980 ): void {
4981 writeChunk(destination, arrayFirstOpenBracket);
4982
4983 let nextArrayOpenBrackChunk = arrayFirstOpenBracket;
4970 - boundaryResources.stylesheets.forEach(resource => {
4984 + hoistableState.stylesheets.forEach(resource => {
4985 if (resource.state === PREAMBLE) {
4986 // We can elide this dependency because it was flushed in the shell and
4987 // should be ready before content is shown on the client
@@ -5214,7 +5228,7 @@ type StylesheetResource = {
5228 state: StylesheetState,
5229 };
5230
5217 -export type BoundaryResources = {
5231 +export type HoistableState = {
5232 styles: Set<StyleQueue>,
5233 stylesheets: Set<StylesheetResource>,
5234 };
@@ -5226,20 +5240,13 @@ export type StyleQueue = {
5240 sheets: Map<string, StylesheetResource>,
5241 };
5242
5229 -export function createBoundaryResources(): BoundaryResources {
5243 +export function createHoistableState(): HoistableState {
5244 return {
5245 styles: new Set(),
5246 stylesheets: new Set(),
5247 };
5248 }
5249
5236 -export function setCurrentlyRenderingBoundaryResourcesTarget(
5237 - renderState: RenderState,
5238 - boundaryResources: null | BoundaryResources,
5239 -) {
5240 - renderState.boundaryResources = boundaryResources;
5241 -}
5242 -
5250 function getResourceKey(href: string): string {
5251 return href;
5252 }
@@ -6087,31 +6094,25 @@ function escapeStringForLinkHeaderQuotedParamValueContextReplacer(
6094 }
6095
6096 function hoistStyleQueueDependency(
6090 - this: BoundaryResources,
6097 + this: HoistableState,
6098 styleQueue: StyleQueue,
6099 ) {
6100 this.styles.add(styleQueue);
6101 }
6102
6103 function hoistStylesheetDependency(
6097 - this: BoundaryResources,
6104 + this: HoistableState,
6105 stylesheet: StylesheetResource,
6106 ) {
6107 this.stylesheets.add(stylesheet);
6108 }
6109
6103 -export function hoistResources(
6104 - renderState: RenderState,
6105 - source: BoundaryResources,
6110 +export function hoistHoistables(
6111 + parentState: HoistableState,
6112 + childState: HoistableState,
6113 ): void {
6107 - const currentBoundaryResources = renderState.boundaryResources;
6108 - if (currentBoundaryResources) {
6109 - source.styles.forEach(hoistStyleQueueDependency, currentBoundaryResources);
6110 - source.stylesheets.forEach(
6111 - hoistStylesheetDependency,
6112 - currentBoundaryResources,
6113 - );
6114 - }
6114 + childState.styles.forEach(hoistStyleQueueDependency, parentState);
6115 + childState.stylesheets.forEach(hoistStylesheetDependency, parentState);
6116 }
6117
6118 // This function is called at various times depending on whether we are rendering
packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js
+8 -14
@@ -10,7 +10,6 @@
10 import type {
11 RenderState as BaseRenderState,
12 ResumableState,
13 - BoundaryResources,
13 StyleQueue,
14 Resource,
15 HeadersDescriptor,
@@ -48,6 +47,7 @@ export type RenderState = {
47 headChunks: null | Array<Chunk | PrecomputedChunk>,
48 externalRuntimeScript: null | any,
49 bootstrapChunks: Array<Chunk | PrecomputedChunk>,
50 + importMapChunks: Array<Chunk | PrecomputedChunk>,
51 onHeaders: void | ((headers: HeadersDescriptor) => void),
52 headers: null | {
53 preconnects: string,
@@ -57,9 +57,7 @@ export type RenderState = {
57 },
58 resets: BaseRenderState['resets'],
59 charsetChunks: Array<Chunk | PrecomputedChunk>,
60 - preconnectChunks: Array<Chunk | PrecomputedChunk>,
61 - importMapChunks: Array<Chunk | PrecomputedChunk>,
62 - preloadChunks: Array<Chunk | PrecomputedChunk>,
60 + viewportChunks: Array<Chunk | PrecomputedChunk>,
61 hoistableChunks: Array<Chunk | PrecomputedChunk>,
62 preconnects: Set<Resource>,
63 fontPreloads: Set<Resource>,
@@ -75,7 +73,6 @@ export type RenderState = {
73 scripts: Map<string, Resource>,
74 moduleScripts: Map<string, Resource>,
75 },
78 - boundaryResources: ?BoundaryResources,
76 stylesToHoist: boolean,
77 // This is an extra field for the legacy renderer
78 generateStaticMarkup: boolean,
@@ -103,13 +100,12 @@ export function createRenderState(
100 headChunks: renderState.headChunks,
101 externalRuntimeScript: renderState.externalRuntimeScript,
102 bootstrapChunks: renderState.bootstrapChunks,
103 + importMapChunks: renderState.importMapChunks,
104 onHeaders: renderState.onHeaders,
105 headers: renderState.headers,
106 resets: renderState.resets,
107 charsetChunks: renderState.charsetChunks,
110 - preconnectChunks: renderState.preconnectChunks,
111 - importMapChunks: renderState.importMapChunks,
112 - preloadChunks: renderState.preloadChunks,
108 + viewportChunks: renderState.viewportChunks,
109 hoistableChunks: renderState.hoistableChunks,
110 preconnects: renderState.preconnects,
111 fontPreloads: renderState.fontPreloads,
@@ -120,7 +116,6 @@ export function createRenderState(
116 scripts: renderState.scripts,
117 bulkPreloads: renderState.bulkPreloads,
118 preloads: renderState.preloads,
123 - boundaryResources: renderState.boundaryResources,
119 stylesToHoist: renderState.stylesToHoist,
120
121 // This is an extra field for the legacy renderer
@@ -138,7 +133,7 @@ export const doctypeChunk: PrecomputedChunk = stringToPrecomputedChunk('');
133
134 export type {
135 ResumableState,
141 - BoundaryResources,
136 + HoistableState,
137 FormatContext,
138 } from './ReactFizzConfigDOM';
139
@@ -158,17 +153,16 @@ export {
153 writeClientRenderBoundaryInstruction,
154 writeStartPendingSuspenseBoundary,
155 writeEndPendingSuspenseBoundary,
161 - writeResourcesForBoundary,
156 + writeHoistablesForBoundary,
157 writePlaceholder,
158 writeCompletedRoot,
159 createRootFormatContext,
160 createResumableState,
166 - createBoundaryResources,
161 + createHoistableState,
162 writePreamble,
163 writeHoistables,
164 writePostamble,
170 - hoistResources,
171 - setCurrentlyRenderingBoundaryResourcesTarget,
165 + hoistHoistables,
166 prepareHostDispatcher,
167 resetResumableState,
168 completeResumableState,
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+281 -110
@@ -671,60 +671,6 @@ describe('ReactDOMFloat', () => {
671 );
672 });
673
674 - // @gate enableFloat
675 - it('emits resources before everything else when rendering with no head', async () => {
676 - function App() {
677 - return (
678 - <>
679 - <title>foo</title>
680 - <link rel="preload" href="foo" as="style" />
681 - </>
682 - );
683 - }
684 -
685 - await act(() => {
686 - buffer = `<!DOCTYPE html><html><head>${ReactDOMFizzServer.renderToString(
687 - <App />,
688 - )}</head><body>foo</body></html>`;
689 - });
690 - expect(getMeaningfulChildren(document)).toEqual(
691 - <html>
692 - <head>
693 - <link rel="preload" href="foo" as="style" />
694 - <title>foo</title>
695 - </head>
696 - <body>foo</body>
697 - </html>,
698 - );
699 - });
700 -
701 - // @gate enableFloat
702 - it('emits resources before everything else when rendering with just a head', async () => {
703 - function App() {
704 - return (
705 - <head>
706 - <title>foo</title>
707 - <link rel="preload" href="foo" as="style" />
708 - </head>
709 - );
710 - }
711 -
712 - await act(() => {
713 - buffer = `<!DOCTYPE html><html>${ReactDOMFizzServer.renderToString(
714 - <App />,
715 - )}<body>foo</body></html>`;
716 - });
717 - expect(getMeaningfulChildren(document)).toEqual(
718 - <html>
719 - <head>
720 - <link rel="preload" href="foo" as="style" />
721 - <title>foo</title>
722 - </head>
723 - <body>foo</body>
724 - </html>,
725 - );
726 - });
727 -
674 // @gate enableFloat
675 it('emits an implicit <head> element to hold resources when none is rendered but an <html> is rendered', async () => {
676 const chunks = [];
@@ -4773,6 +4719,167 @@ body {
4719 );
4720 });
4721
4722 + it('does not flush hoistables for fallbacks', async () => {
4723 + function App() {
4724 + return (
4725 + <html>
4726 + <body>
4727 + <Suspense
4728 + fallback={
4729 + <>
4730 + <div>fallback1</div>
4731 + <meta name="fallback1" />
4732 + <title>foo</title>
4733 + </>
4734 + }>
4735 + <>
4736 + <div>primary1</div>
4737 + <meta name="primary1" />
4738 + </>
4739 + </Suspense>
4740 + <Suspense
4741 + fallback={
4742 + <>
4743 + <div>fallback2</div>
4744 + <meta name="fallback2" />
4745 + <link rel="foo" href="bar" />
4746 + </>
4747 + }>
4748 + <>
4749 + <div>primary2</div>
4750 + <BlockedOn value="first">
4751 + <meta name="primary2" />
4752 + </BlockedOn>
4753 + </>
4754 + </Suspense>
4755 + <Suspense
4756 + fallback={
4757 + <>
4758 + <div>fallback3</div>
4759 + <meta name="fallback3" />
4760 + <Suspense fallback="deep">
4761 + <div>deep fallback ... primary content</div>
4762 + <meta name="deep fallback" />
4763 + </Suspense>
4764 + </>
4765 + }>
4766 + <>
4767 + <div>primary3</div>
4768 + <BlockedOn value="second">
4769 + <meta name="primary3" />
4770 + </BlockedOn>
4771 + </>
4772 + </Suspense>
4773 + </body>
4774 + </html>
4775 + );
4776 + }
4777 +
4778 + await act(() => {
4779 + renderToPipeableStream(<App />).pipe(writable);
4780 + resolveText('first');
4781 + });
4782 +
4783 + expect(getMeaningfulChildren(document)).toEqual(
4784 + <html>
4785 + <head>
4786 + <meta name="primary1" />
4787 + <meta name="primary2" />
4788 + </head>
4789 + <body>
4790 + <div>primary1</div>
4791 + <div>primary2</div>
4792 + <div>fallback3</div>
4793 + <div>deep fallback ... primary content</div>
4794 + </body>
4795 + </html>,
4796 + );
4797 +
4798 + await act(() => {
4799 + resolveText('second');
4800 + });
4801 +
4802 + expect(getMeaningfulChildren(document)).toEqual(
4803 + <html>
4804 + <head>
4805 + <meta name="primary1" />
4806 + <meta name="primary2" />
4807 + </head>
4808 + <body>
4809 + <div>primary1</div>
4810 + <div>primary2</div>
4811 + <div>primary3</div>
4812 + <meta name="primary3" />
4813 + </body>
4814 + </html>,
4815 + );
4816 + });
4817 +
4818 + it('avoids flushing hoistables from completed boundaries nested inside fallbacks', async () => {
4819 + function App() {
4820 + return (
4821 + <html>
4822 + <body>
4823 + <Suspense
4824 + fallback={
4825 + <Suspense
4826 + fallback={
4827 + <>
4828 + <div>nested fallback1</div>
4829 + <meta name="nested fallback1" />
4830 + </>
4831 + }>
4832 + <>
4833 + <div>nested primary1</div>
4834 + <meta name="nested primary1" />
4835 + </>
4836 + </Suspense>
4837 + }>
4838 + <BlockedOn value="release" />
4839 + <>
4840 + <div>primary1</div>
4841 + <meta name="primary1" />
4842 + </>
4843 + </Suspense>
4844 + </body>
4845 + </html>
4846 + );
4847 + }
4848 +
4849 + await act(() => {
4850 + renderToPipeableStream(<App />).pipe(writable);
4851 + });
4852 +
4853 + expect(getMeaningfulChildren(document)).toEqual(
4854 + <html>
4855 + <head>
4856 + {/* The primary content hoistables emit */}
4857 + <meta name="primary1" />
4858 + </head>
4859 + <body>
4860 + {/* The fallback content emits but the hoistables do not even if they
4861 + inside a nested suspense boundary that is resolved */}
4862 + <div>nested primary1</div>
4863 + </body>
4864 + </html>,
4865 + );
4866 +
4867 + await act(() => {
4868 + resolveText('release');
4869 + });
4870 +
4871 + expect(getMeaningfulChildren(document)).toEqual(
4872 + <html>
4873 + <head>
4874 + <meta name="primary1" />
4875 + </head>
4876 + <body>
4877 + <div>primary1</div>
4878 + </body>
4879 + </html>,
4880 + );
4881 + });
4882 +
4883 describe('ReactDOM.prefetchDNS(href)', () => {
4884 it('creates a dns-prefetch resource when called', async () => {
4885 function App({url}) {
@@ -4840,6 +4947,120 @@ body {
4947 });
4948 });
4949
4950 + it('does not wait for stylesheets of completed fallbacks', async () => {
4951 + function Unblock({value}) {
4952 + resolveText(value);
4953 + return null;
4954 + }
4955 + function App() {
4956 + return (
4957 + <html>
4958 + <body>
4959 + <Suspense fallback="loading...">
4960 + <div>hello world</div>
4961 + <BlockedOn value="unblock inner boundaries">
4962 + <Suspense
4963 + fallback={
4964 + <>
4965 + <link
4966 + rel="stylesheet"
4967 + href="completed inner"
4968 + precedence="default"
4969 + />
4970 + <div>inner fallback</div>
4971 + <Unblock value="completed inner" />
4972 + </>
4973 + }>
4974 + <BlockedOn value="completed inner" />
4975 + <div>inner boundary</div>
4976 + </Suspense>
4977 + <Suspense
4978 + fallback={
4979 + <>
4980 + <link
4981 + rel="stylesheet"
4982 + href="in fallback inner"
4983 + precedence="default"
4984 + />
4985 + <div>inner blocked fallback</div>
4986 + </>
4987 + }>
4988 + <BlockedOn value="in fallback inner" />
4989 + <div>inner blocked boundary</div>
4990 + </Suspense>
4991 + </BlockedOn>
4992 + <BlockedOn value="complete root" />
4993 + </Suspense>
4994 + </body>
4995 + </html>
4996 + );
4997 + }
4998 +
4999 + await act(() => {
5000 + renderToPipeableStream(<App />).pipe(writable);
5001 + });
5002 +
5003 + expect(getMeaningfulChildren(document)).toEqual(
5004 + <html>
5005 + <head />
5006 + <body>loading...</body>
5007 + </html>,
5008 + );
5009 +
5010 + await act(async () => {
5011 + resolveText('unblock inner boundaries');
5012 + });
5013 + expect(getMeaningfulChildren(document)).toEqual(
5014 + <html>
5015 + <head />
5016 + <body>
5017 + loading...
5018 + <link rel="preload" href="completed inner" as="style" />
5019 + <link rel="preload" href="in fallback inner" as="style" />
5020 + </body>
5021 + </html>,
5022 + );
5023 +
5024 + await act(() => {
5025 + resolveText('completed inner');
5026 + });
5027 + expect(getMeaningfulChildren(document)).toEqual(
5028 + <html>
5029 + <head />
5030 + <body>
5031 + loading...
5032 + <link rel="preload" href="completed inner" as="style" />
5033 + <link rel="preload" href="in fallback inner" as="style" />
5034 + </body>
5035 + </html>,
5036 + );
5037 +
5038 + await act(() => {
5039 + resolveText('complete root');
5040 + });
5041 + await act(() => {
5042 + loadStylesheets();
5043 + });
5044 + expect(getMeaningfulChildren(document)).toEqual(
5045 + <html>
5046 + <head>
5047 + <link
5048 + rel="stylesheet"
5049 + href="in fallback inner"
5050 + data-precedence="default"
5051 + />
5052 + </head>
5053 + <body>
5054 + <div>hello world</div>
5055 + <div>inner boundary</div>
5056 + <div>inner blocked fallback</div>
5057 + <link rel="preload" href="completed inner" as="style" />
5058 + <link rel="preload" href="in fallback inner" as="style" />
5059 + </body>
5060 + </html>,
5061 + );
5062 + });
5063 +
5064 describe('ReactDOM.preconnect(href, { crossOrigin })', () => {
5065 it('creates a preconnect resource when called', async () => {
5066 function App({url}) {
@@ -7746,6 +7967,7 @@ background-color: green;
7967 <link rel="preload" href="foo" as="style" />
7968 <link rel="preconnect" href="bar" />
7969 <link rel="dns-prefetch" href="baz" />
7970 + <meta name="viewport" />
7971 <meta charSet="utf-8" />
7972 </body>
7973 </html>,
@@ -7758,72 +7980,21 @@ background-color: green;
7980 <head>
7981 {/* charset first */}
7982 <meta charset="utf-8" />
7761 - {/* preconnect links next */}
7762 - <link rel="preconnect" href="bar" />
7763 - <link rel="dns-prefetch" href="baz" />
7764 - {/* preloads next */}
7765 - <link rel="preload" href="foo" as="style" />
7983 + {/* viewport meta next */}
7984 + <meta name="viewport" />
7985 {/* Everything else last */}
7986 <link rel="foo" href="foo" />
7987 <meta name="bar" />
7988 <title>a title</title>
7989 + <link rel="preload" href="foo" as="style" />
7990 + <link rel="preconnect" href="bar" />
7991 + <link rel="dns-prefetch" href="baz" />
7992 </head>
7993 <body />
7994 </html>,
7995 );
7996 });
7997
7776 - // @gate enableFloat
7777 - it('emits hoistables before other content when streaming in late', async () => {
7778 - let content = '';
7779 - writable.on('data', chunk => (content += chunk));
7780 -
7781 - await act(() => {
7782 - const {pipe} = renderToPipeableStream(
7783 - <html>
7784 - <body>
7785 - <meta name="early" />
7786 - <Suspense fallback={null}>
7787 - <BlockedOn value="foo">
7788 - <div>foo</div>
7789 - <meta name="late" />
7790 - </BlockedOn>
7791 - </Suspense>
7792 - </body>
7793 - </html>,
7794 - );
7795 - pipe(writable);
7796 - });
7797 -
7798 - expect(getMeaningfulChildren(document)).toEqual(
7799 - <html>
7800 - <head>
7801 - <meta name="early" />
7802 - </head>
7803 - <body />
7804 - </html>,
7805 - );
7806 - content = '';
7807 -
7808 - await act(() => {
7809 - resolveText('foo');
7810 - });
7811 -
7812 - expect(content.slice(0, 30)).toEqual('<meta name="late"/><div hidden');
7813 -
7814 - expect(getMeaningfulChildren(document)).toEqual(
7815 - <html>
7816 - <head>
7817 - <meta name="early" />
7818 - </head>
7819 - <body>
7820 - <div>foo</div>
7821 - <meta name="late" />
7822 - </body>
7823 - </html>,
7824 - );
7825 - });
7826 -
7998 // @gate enableFloat
7999 it('supports rendering hoistables outside of <html> scope', async () => {
8000 await act(() => {
packages/react-noop-renderer/src/ReactNoopServer.js
+6 -7
@@ -52,7 +52,7 @@ type Destination = {
52 };
53
54 type RenderState = null;
55 -type BoundaryResources = null;
55 +type HoistableState = null;
56
57 const POP = Buffer.from('/', 'utf8');
58
@@ -261,17 +261,16 @@ const ReactNoopServer = ReactFizzServer({
261 boundary.status = 'client-render';
262 },
263
264 + prepareHostDispatcher() {},
265 +
266 writePreamble() {},
267 writeHoistables() {},
268 + writeHoistablesForBoundary() {},
269 writePostamble() {},
267 -
268 - createBoundaryResources(): BoundaryResources {
270 + hoistHoistables(parent: HoistableState, child: HoistableState) {},
271 + createHoistableState(): HoistableState {
272 return null;
273 },
271 -
272 - setCurrentlyRenderingBoundaryResourcesTarget(resources: BoundaryResources) {},
273 -
274 - prepareHostDispatcher() {},
274 emitEarlyPreloads() {},
275 });
276
packages/react-server/src/ReactFizzServer.js
+95 -85
@@ -26,7 +26,7 @@ import type {
26 RenderState,
27 ResumableState,
28 FormatContext,
29 - BoundaryResources,
29 + HoistableState,
30 } from './ReactFizzConfig';
31 import type {ContextSnapshot} from './ReactFizzNewContext';
32 import type {ComponentStackNode} from './ReactFizzComponentStack';
@@ -57,6 +57,7 @@ import {
57 writeClientRenderBoundaryInstruction,
58 writeCompletedBoundaryInstruction,
59 writeCompletedSegmentInstruction,
60 + writeHoistablesForBoundary,
61 pushTextInstance,
62 pushStartInstance,
63 pushEndInstance,
@@ -64,13 +65,11 @@ import {
65 pushEndCompletedSuspenseBoundary,
66 pushSegmentFinale,
67 getChildFormatContext,
67 - writeResourcesForBoundary,
68 - writePreamble,
68 writeHoistables,
69 + writePreamble,
70 writePostamble,
71 - hoistResources,
72 - setCurrentlyRenderingBoundaryResourcesTarget,
73 - createBoundaryResources,
71 + hoistHoistables,
72 + createHoistableState,
73 prepareHostDispatcher,
74 supportsRequestStorage,
75 requestStorage,
@@ -211,7 +210,8 @@ type SuspenseBoundary = {
210 completedSegments: Array<Segment>, // completed but not yet flushed segments.
211 byteSize: number, // used to determine whether to inline children boundaries.
212 fallbackAbortableTasks: Set<Task>, // used to cancel task on the fallback if the boundary completes or gets canceled.
214 - resources: BoundaryResources,
213 + contentState: HoistableState,
214 + fallbackState: HoistableState,
215 trackedContentKeyPath: null | KeyNode, // used to track the path for replay nodes
216 trackedFallbackNode: null | ReplayNode, // used to track the fallback for replay nodes
217 };
@@ -223,6 +223,7 @@ type RenderTask = {
223 ping: () => void,
224 blockedBoundary: Root | SuspenseBoundary,
225 blockedSegment: Segment, // the segment we'll write to
226 + hoistableState: null | HoistableState, // Boundary state we'll mutate while rendering. This may not equal the state of the blockedBoundary
227 abortSet: Set<Task>, // the abortable set that this task belongs to
228 keyPath: Root | KeyNode, // the path of all parent keys currently rendering
229 formatContext: FormatContext, // the format's specific context (e.g. HTML/SVG/MathML)
@@ -231,6 +232,7 @@ type RenderTask = {
232 treeContext: TreeContext, // the current tree context that this task is executing in
233 componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component
234 thenableState: null | ThenableState,
235 + isFallback: boolean, // whether this task is rendering inside a fallback tree
236 };
237
238 type ReplaySet = {
@@ -248,6 +250,7 @@ type ReplayTask = {
250 ping: () => void,
251 blockedBoundary: Root | SuspenseBoundary,
252 blockedSegment: null, // we don't write to anything when we replay
253 + hoistableState: null | HoistableState, // Boundary state we'll mutate while rendering. This may not equal the state of the blockedBoundary
254 abortSet: Set<Task>, // the abortable set that this task belongs to
255 keyPath: Root | KeyNode, // the path of all parent keys currently rendering
256 formatContext: FormatContext, // the format's specific context (e.g. HTML/SVG/MathML)
@@ -256,6 +259,7 @@ type ReplayTask = {
259 treeContext: TreeContext, // the current tree context that this task is executing in
260 componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component
261 thenableState: null | ThenableState,
262 + isFallback: boolean, // whether this task is rendering inside a fallback tree
263 };
264
265 export type Task = RenderTask | ReplayTask;
@@ -421,6 +425,7 @@ export function createRequest(
425 -1,
426 null,
427 rootSegment,
428 + null,
429 abortSet,
430 null,
431 rootFormatContext,
@@ -428,6 +433,7 @@ export function createRequest(
433 rootContextSnapshot,
434 emptyTreeContext,
435 null,
436 + false,
437 );
438 pingedTasks.push(rootTask);
439 return request;
@@ -532,6 +538,7 @@ export function resumeRequest(
538 -1,
539 null,
540 rootSegment,
541 + null,
542 abortSet,
543 null,
544 postponedState.rootFormatContext,
@@ -539,6 +546,7 @@ export function resumeRequest(
546 rootContextSnapshot,
547 emptyTreeContext,
548 null,
549 + false,
550 );
551 pingedTasks.push(rootTask);
552 return request;
@@ -556,6 +564,7 @@ export function resumeRequest(
564 children,
565 -1,
566 null,
567 + null,
568 abortSet,
569 null,
570 postponedState.rootFormatContext,
@@ -563,6 +572,7 @@ export function resumeRequest(
572 rootContextSnapshot,
573 emptyTreeContext,
574 null,
575 + false,
576 );
577 pingedTasks.push(rootTask);
578 return request;
@@ -601,7 +611,8 @@ function createSuspenseBoundary(
611 byteSize: 0,
612 fallbackAbortableTasks,
613 errorDigest: null,
604 - resources: createBoundaryResources(),
614 + contentState: createHoistableState(),
615 + fallbackState: createHoistableState(),
616 trackedContentKeyPath: null,
617 trackedFallbackNode: null,
618 };
@@ -614,6 +625,7 @@ function createRenderTask(
625 childIndex: number,
626 blockedBoundary: Root | SuspenseBoundary,
627 blockedSegment: Segment,
628 + hoistableState: null | HoistableState,
629 abortSet: Set<Task>,
630 keyPath: Root | KeyNode,
631 formatContext: FormatContext,
@@ -621,6 +633,7 @@ function createRenderTask(
633 context: ContextSnapshot,
634 treeContext: TreeContext,
635 componentStack: null | ComponentStackNode,
636 + isFallback: boolean,
637 ): RenderTask {
638 request.allPendingTasks++;
639 if (blockedBoundary === null) {
@@ -635,6 +648,7 @@ function createRenderTask(
648 ping: () => pingTask(request, task),
649 blockedBoundary,
650 blockedSegment,
651 + hoistableState,
652 abortSet,
653 keyPath,
654 formatContext,
@@ -643,6 +657,7 @@ function createRenderTask(
657 treeContext,
658 componentStack,
659 thenableState,
660 + isFallback,
661 };
662 abortSet.add(task);
663 return task;
@@ -655,6 +670,7 @@ function createReplayTask(
670 node: ReactNodeList,
671 childIndex: number,
672 blockedBoundary: Root | SuspenseBoundary,
673 + hoistableState: null | HoistableState,
674 abortSet: Set<Task>,
675 keyPath: Root | KeyNode,
676 formatContext: FormatContext,
@@ -662,6 +678,7 @@ function createReplayTask(
678 context: ContextSnapshot,
679 treeContext: TreeContext,
680 componentStack: null | ComponentStackNode,
681 + isFallback: boolean,
682 ): ReplayTask {
683 request.allPendingTasks++;
684 if (blockedBoundary === null) {
@@ -677,6 +694,7 @@ function createReplayTask(
694 ping: () => pingTask(request, task),
695 blockedBoundary,
696 blockedSegment: null,
697 + hoistableState,
698 abortSet,
699 keyPath,
700 formatContext,
@@ -685,6 +703,7 @@ function createReplayTask(
703 treeContext,
704 componentStack,
705 thenableState,
706 + isFallback,
707 };
708 abortSet.add(task);
709 return task;
@@ -892,6 +911,7 @@ function renderSuspenseBoundary(
911
912 const prevKeyPath = task.keyPath;
913 const parentBoundary = task.blockedBoundary;
914 + const parentHoistableState = task.hoistableState;
915 const parentSegment = task.blockedSegment;
916
917 // Each time we enter a suspense boundary, we split out into a new segment for
@@ -944,13 +964,8 @@ function renderSuspenseBoundary(
964 // context switching. We just need to temporarily switch which boundary and which segment
965 // we're writing to. If something suspends, it'll spawn new suspended task with that context.
966 task.blockedBoundary = newBoundary;
967 + task.hoistableState = newBoundary.contentState;
968 task.blockedSegment = contentRootSegment;
948 - if (enableFloat) {
949 - setCurrentlyRenderingBoundaryResourcesTarget(
950 - request.renderState,
951 - newBoundary.resources,
952 - );
953 - }
969 task.keyPath = keyPath;
970
971 try {
@@ -1000,13 +1015,8 @@ function renderSuspenseBoundary(
1015 // We don't need to schedule any task because we know the parent has written yet.
1016 // We do need to fallthrough to create the fallback though.
1017 } finally {
1003 - if (enableFloat) {
1004 - setCurrentlyRenderingBoundaryResourcesTarget(
1005 - request.renderState,
1006 - parentBoundary ? parentBoundary.resources : null,
1007 - );
1008 - }
1018 task.blockedBoundary = parentBoundary;
1019 + task.hoistableState = parentHoistableState;
1020 task.blockedSegment = parentSegment;
1021 task.keyPath = prevKeyPath;
1022 task.componentStack = previousComponentStack;
@@ -1043,6 +1053,7 @@ function renderSuspenseBoundary(
1053 -1,
1054 parentBoundary,
1055 boundarySegment,
1056 + newBoundary.fallbackState,
1057 fallbackAbortSet,
1058 fallbackKeyPath,
1059 task.formatContext,
@@ -1052,6 +1063,7 @@ function renderSuspenseBoundary(
1063 // This stack should be the Suspense boundary stack because while the fallback is actually a child segment
1064 // of the parent boundary from a component standpoint the fallback is a child of the Suspense boundary itself
1065 suspenseComponentStack,
1066 + true,
1067 );
1068 // TODO: This should be queued at a separate lower priority queue so that we only work
1069 // on preparing fallbacks if we don't have any more main content to task on.
@@ -1079,6 +1091,7 @@ function replaySuspenseBoundary(
1091 const previousReplaySet: ReplaySet = task.replay;
1092
1093 const parentBoundary = task.blockedBoundary;
1094 + const parentHoistableState = task.hoistableState;
1095
1096 const content: ReactNodeList = props.children;
1097 const fallback: ReactNodeList = props.fallback;
@@ -1093,13 +1106,8 @@ function replaySuspenseBoundary(
1106 // context switching. We just need to temporarily switch which boundary and replay node
1107 // we're writing to. If something suspends, it'll spawn new suspended task with that context.
1108 task.blockedBoundary = resumedBoundary;
1109 + task.hoistableState = resumedBoundary.contentState;
1110 task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1};
1097 - if (enableFloat) {
1098 - setCurrentlyRenderingBoundaryResourcesTarget(
1099 - request.renderState,
1100 - resumedBoundary.resources,
1101 - );
1102 - }
1111
1112 try {
1113 // We use the safe form because we don't handle suspending here. Only error handling.
@@ -1154,13 +1162,8 @@ function replaySuspenseBoundary(
1162 // We don't need to schedule any task because we know the parent has written yet.
1163 // We do need to fallthrough to create the fallback though.
1164 } finally {
1157 - if (enableFloat) {
1158 - setCurrentlyRenderingBoundaryResourcesTarget(
1159 - request.renderState,
1160 - parentBoundary ? parentBoundary.resources : null,
1161 - );
1162 - }
1165 task.blockedBoundary = parentBoundary;
1166 + task.hoistableState = parentHoistableState;
1167 task.replay = previousReplaySet;
1168 task.keyPath = prevKeyPath;
1169 task.componentStack = previousComponentStack;
@@ -1182,6 +1185,7 @@ function replaySuspenseBoundary(
1185 fallback,
1186 -1,
1187 parentBoundary,
1188 + resumedBoundary.fallbackState,
1189 fallbackAbortSet,
1190 fallbackKeyPath,
1191 task.formatContext,
@@ -1191,6 +1195,7 @@ function replaySuspenseBoundary(
1195 // This stack should be the Suspense boundary stack because while the fallback is actually a child segment
1196 // of the parent boundary from a component standpoint the fallback is a child of the Suspense boundary itself
1197 suspenseComponentStack,
1198 + true,
1199 );
1200 // TODO: This should be queued at a separate lower priority queue so that we only work
1201 // on preparing fallbacks if we don't have any more main content to task on.
@@ -1257,8 +1262,10 @@ function renderHostElement(
1262 props,
1263 request.resumableState,
1264 request.renderState,
1265 + task.hoistableState,
1266 task.formatContext,
1267 segment.lastPushedText,
1268 + task.isFallback,
1269 );
1270 segment.lastPushedText = false;
1271 const prevContext = task.formatContext;
@@ -2683,6 +2690,7 @@ function spawnNewSuspendedReplayTask(
2690 task.node,
2691 task.childIndex,
2692 task.blockedBoundary,
2693 + task.hoistableState,
2694 task.abortSet,
2695 task.keyPath,
2696 task.formatContext,
@@ -2692,6 +2700,7 @@ function spawnNewSuspendedReplayTask(
2700 // We pop one task off the stack because the node that suspended will be tried again,
2701 // which will add it back onto the stack.
2702 task.componentStack !== null ? task.componentStack.parent : null,
2703 + task.isFallback,
2704 );
2705
2706 const ping = newTask.ping;
@@ -2727,6 +2736,7 @@ function spawnNewSuspendedRenderTask(
2736 task.childIndex,
2737 task.blockedBoundary,
2738 newSegment,
2739 + task.hoistableState,
2740 task.abortSet,
2741 task.keyPath,
2742 task.formatContext,
@@ -2736,6 +2746,7 @@ function spawnNewSuspendedRenderTask(
2746 // We pop one task off the stack because the node that suspended will be tried again,
2747 // which will add it back onto the stack.
2748 task.componentStack !== null ? task.componentStack.parent : null,
2749 + task.isFallback,
2750 );
2751
2752 const ping = newTask.ping;
@@ -3345,13 +3356,6 @@ function finishedTask(
3356 }
3357
3358 function retryTask(request: Request, task: Task): void {
3348 - if (enableFloat) {
3349 - const blockedBoundary = task.blockedBoundary;
3350 - setCurrentlyRenderingBoundaryResourcesTarget(
3351 - request.renderState,
3352 - blockedBoundary ? blockedBoundary.resources : null,
3353 - );
3354 - }
3359 const segment = task.blockedSegment;
3360 if (segment === null) {
3361 retryReplayTask(
@@ -3456,9 +3460,6 @@ function retryRenderTask(
3460 erroredTask(request, task.blockedBoundary, x, errorInfo);
3461 return;
3462 } finally {
3459 - if (enableFloat) {
3460 - setCurrentlyRenderingBoundaryResourcesTarget(request.renderState, null);
3461 - }
3463 if (__DEV__) {
3464 currentTaskInDEV = prevTaskInDEV;
3465 }
@@ -3541,9 +3542,6 @@ function retryReplayTask(request: Request, task: ReplayTask): void {
3542 }
3543 return;
3544 } finally {
3544 - if (enableFloat) {
3545 - setCurrentlyRenderingBoundaryResourcesTarget(request.renderState, null);
3546 - }
3545 if (__DEV__) {
3546 currentTaskInDEV = prevTaskInDEV;
3547 }
@@ -3612,10 +3610,26 @@ export function performWork(request: Request): void {
3610 }
3611 }
3612
3613 +function flushPreamble(
3614 + request: Request,
3615 + destination: Destination,
3616 + rootSegment: Segment,
3617 +) {
3618 + const willFlushAllSegments =
3619 + request.allPendingTasks === 0 && request.trackedPostpones === null;
3620 + writePreamble(
3621 + destination,
3622 + request.resumableState,
3623 + request.renderState,
3624 + willFlushAllSegments,
3625 + );
3626 +}
3627 +
3628 function flushSubtree(
3629 request: Request,
3630 destination: Destination,
3631 segment: Segment,
3632 + hoistableState: null | HoistableState,
3633 ): boolean {
3634 segment.parentFlushed = true;
3635 switch (segment.status) {
@@ -3645,7 +3659,7 @@ function flushSubtree(
3659 for (; chunkIdx < nextChild.index; chunkIdx++) {
3660 writeChunk(destination, chunks[chunkIdx]);
3661 }
3648 - r = flushSegment(request, destination, nextChild);
3662 + r = flushSegment(request, destination, nextChild, hoistableState);
3663 }
3664 // Finally just write all the remaining chunks
3665 for (; chunkIdx < chunks.length - 1; chunkIdx++) {
@@ -3668,11 +3682,12 @@ function flushSegment(
3682 request: Request,
3683 destination: Destination,
3684 segment: Segment,
3685 + hoistableState: null | HoistableState,
3686 ): boolean {
3687 const boundary = segment.boundary;
3688 if (boundary === null) {
3689 // Not a suspense boundary.
3675 - return flushSubtree(request, destination, segment);
3690 + return flushSubtree(request, destination, segment, hoistableState);
3691 }
3692
3693 boundary.parentFlushed = true;
@@ -3690,7 +3705,7 @@ function flushSegment(
3705 boundary.errorComponentStack,
3706 );
3707 // Flush the fallback.
3693 - flushSubtree(request, destination, segment);
3708 + flushSubtree(request, destination, segment, hoistableState);
3709
3710 return writeEndClientRenderedSuspenseBoundary(
3711 destination,
@@ -3713,8 +3728,15 @@ function flushSegment(
3728 const id = boundary.rootSegmentID;
3729 writeStartPendingSuspenseBoundary(destination, request.renderState, id);
3730
3731 + // We are going to flush the fallback so we need to hoist the fallback
3732 + // state to the parent boundary
3733 + if (enableFloat) {
3734 + if (hoistableState) {
3735 + hoistHoistables(hoistableState, boundary.fallbackState);
3736 + }
3737 + }
3738 // Flush the fallback.
3717 - flushSubtree(request, destination, segment);
3739 + flushSubtree(request, destination, segment, hoistableState);
3740
3741 return writeEndPendingSuspenseBoundary(destination, request.renderState);
3742 } else if (boundary.byteSize > request.progressiveChunkSize) {
@@ -3735,13 +3757,20 @@ function flushSegment(
3757 boundary.rootSegmentID,
3758 );
3759
3760 + // While we are going to flush the fallback we are going to follow it up with
3761 + // the completed boundary immediately so we make the choice to omit fallback
3762 + // boundary state from the parent since it will be replaced when the boundary
3763 + // flushes later in this pass or in a future flush
3764 +
3765 // Flush the fallback.
3739 - flushSubtree(request, destination, segment);
3766 + flushSubtree(request, destination, segment, hoistableState);
3767
3768 return writeEndPendingSuspenseBoundary(destination, request.renderState);
3769 } else {
3770 if (enableFloat) {
3744 - hoistResources(request.renderState, boundary.resources);
3771 + if (hoistableState) {
3772 + hoistHoistables(hoistableState, boundary.contentState);
3773 + }
3774 }
3775 // We can inline this boundary's content as a complete boundary.
3776 writeStartCompletedSuspenseBoundary(destination, request.renderState);
@@ -3755,7 +3784,7 @@ function flushSegment(
3784 }
3785
3786 const contentSegment = completedSegments[0];
3758 - flushSegment(request, destination, contentSegment);
3787 + flushSegment(request, destination, contentSegment, hoistableState);
3788
3789 return writeEndCompletedSuspenseBoundary(destination, request.renderState);
3790 }
@@ -3781,6 +3810,7 @@ function flushSegmentContainer(
3810 request: Request,
3811 destination: Destination,
3812 segment: Segment,
3813 + hoistableState: HoistableState,
3814 ): boolean {
3815 writeStartSegment(
3816 destination,
@@ -3788,7 +3818,7 @@ function flushSegmentContainer(
3818 segment.parentFormatContext,
3819 segment.id,
3820 );
3791 - flushSegment(request, destination, segment);
3821 + flushSegment(request, destination, segment, hoistableState);
3822 return writeEndSegment(destination, segment.parentFormatContext);
3823 }
3824
@@ -3797,12 +3827,6 @@ function flushCompletedBoundary(
3827 destination: Destination,
3828 boundary: SuspenseBoundary,
3829 ): boolean {
3800 - if (enableFloat) {
3801 - setCurrentlyRenderingBoundaryResourcesTarget(
3802 - request.renderState,
3803 - boundary.resources,
3804 - );
3805 - }
3830 const completedSegments = boundary.completedSegments;
3831 let i = 0;
3832 for (; i < completedSegments.length; i++) {
@@ -3812,9 +3836,9 @@ function flushCompletedBoundary(
3836 completedSegments.length = 0;
3837
3838 if (enableFloat) {
3815 - writeResourcesForBoundary(
3839 + writeHoistablesForBoundary(
3840 destination,
3817 - boundary.resources,
3841 + boundary.contentState,
3842 request.renderState,
3843 );
3844 }
@@ -3824,7 +3848,7 @@ function flushCompletedBoundary(
3848 request.resumableState,
3849 request.renderState,
3850 boundary.rootSegmentID,
3827 - boundary.resources,
3851 + boundary.contentState,
3852 );
3853 }
3854
@@ -3833,12 +3857,6 @@ function flushPartialBoundary(
3857 destination: Destination,
3858 boundary: SuspenseBoundary,
3859 ): boolean {
3836 - if (enableFloat) {
3837 - setCurrentlyRenderingBoundaryResourcesTarget(
3838 - request.renderState,
3839 - boundary.resources,
3840 - );
3841 - }
3860 const completedSegments = boundary.completedSegments;
3861 let i = 0;
3862 for (; i < completedSegments.length; i++) {
@@ -3856,13 +3874,9 @@ function flushPartialBoundary(
3874 completedSegments.splice(0, i);
3875
3876 if (enableFloat) {
3859 - // The way this is structured we only write resources for partial boundaries
3860 - // if there is no backpressure. Later before we complete the boundary we
3861 - // will write resources regardless of backpressure before we emit the
3862 - // completion instruction
3863 - return writeResourcesForBoundary(
3877 + return writeHoistablesForBoundary(
3878 destination,
3865 - boundary.resources,
3879 + boundary.contentState,
3880 request.renderState,
3881 );
3882 } else {
@@ -3881,6 +3895,8 @@ function flushPartiallyCompletedSegment(
3895 return true;
3896 }
3897
3898 + const hoistableState = boundary.contentState;
3899 +
3900 const segmentID = segment.id;
3901 if (segmentID === -1) {
3902 // This segment wasn't previously referred to. This happens at the root of
@@ -3893,13 +3909,13 @@ function flushPartiallyCompletedSegment(
3909 );
3910 }
3911
3896 - return flushSegmentContainer(request, destination, segment);
3912 + return flushSegmentContainer(request, destination, segment, hoistableState);
3913 } else if (segmentID === boundary.rootSegmentID) {
3914 // When we emit postponed boundaries, we might have assigned the ID already
3915 // but it's still the root segment so we can't inject it into the parent yet.
3900 - return flushSegmentContainer(request, destination, segment);
3916 + return flushSegmentContainer(request, destination, segment, hoistableState);
3917 } else {
3902 - flushSegmentContainer(request, destination, segment);
3918 + flushSegmentContainer(request, destination, segment, hoistableState);
3919 return writeCompletedSegmentInstruction(
3920 destination,
3921 request.resumableState,
@@ -3928,15 +3944,10 @@ function flushCompletedQueues(
3944 return;
3945 } else if (request.pendingRootTasks === 0) {
3946 if (enableFloat) {
3931 - writePreamble(
3932 - destination,
3933 - request.resumableState,
3934 - request.renderState,
3935 - request.allPendingTasks === 0 && request.trackedPostpones === null,
3936 - );
3947 + flushPreamble(request, destination, completedRootSegment);
3948 }
3949
3939 - flushSegment(request, destination, completedRootSegment);
3950 + flushSegment(request, destination, completedRootSegment, null);
3951 request.completedRootSegment = null;
3952 writeCompletedRoot(destination, request.renderState);
3953 } else {
@@ -3944,7 +3955,6 @@ function flushCompletedQueues(
3955 return;
3956 }
3957 }
3947 -
3958 if (enableFloat) {
3959 writeHoistables(destination, request.resumableState, request.renderState);
3960 }
packages/react-server/src/forks/ReactFizzConfig.custom.js
+4 -7
@@ -29,8 +29,8 @@ import type {TransitionStatus} from 'react-reconciler/src/ReactFiberConfig';
29 declare var $$$config: any;
30 export opaque type Destination = mixed; // eslint-disable-line no-undef
31 export opaque type RenderState = mixed;
32 +export opaque type HoistableState = mixed;
33 export opaque type ResumableState = mixed;
33 -export opaque type BoundaryResources = mixed;
34 export opaque type FormatContext = mixed;
35 export opaque type HeadersDescriptor = mixed;
36 export type {TransitionStatus};
@@ -86,11 +86,8 @@ export const NotPendingTransition = $$$config.NotPendingTransition;
86 // -------------------------
87 export const writePreamble = $$$config.writePreamble;
88 export const writeHoistables = $$$config.writeHoistables;
89 +export const writeHoistablesForBoundary = $$$config.writeHoistablesForBoundary;
90 export const writePostamble = $$$config.writePostamble;
90 -export const hoistResources = $$$config.hoistResources;
91 -export const createResources = $$$config.createResources;
92 -export const createBoundaryResources = $$$config.createBoundaryResources;
93 -export const setCurrentlyRenderingBoundaryResourcesTarget =
94 - $$$config.setCurrentlyRenderingBoundaryResourcesTarget;
95 -export const writeResourcesForBoundary = $$$config.writeResourcesForBoundary;
91 +export const hoistHoistables = $$$config.hoistHoistables;
92 +export const createHoistableState = $$$config.createHoistableState;
93 export const emitEarlyPreloads = $$$config.emitEarlyPreloads;