@samitouri / QOS-React-1 / commits / b775564d35

[Fizz] Ensure Resumable State is Serializable (#27388)

Moves writing queues to renderState. We shouldn't need the resource tracking's value. We just need to know if that resource has already been emitted. We can use a Set for this. To ensure that set is directly serializable we can just use a dictionary-like object with no value. See individual commits for special cases.

Sebastian Markbåge committed Sep 20, 2023 at 12:21 UTC b775564d35d6fe9f9acead0f380bbc86bb6bdd2c
15 files changed +575 -340
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+338 -274
@@ -52,6 +52,7 @@ import {
52 import {
53 resolveRequest,
54 getResumableState,
55 + getRenderState,
56 flushResources,
57 } from 'react-server/src/ReactFizzServer';
58
@@ -135,6 +136,10 @@ export type RenderState = {
136 htmlChunks: null | Array<Chunk | PrecomputedChunk>,
137 headChunks: null | Array<Chunk | PrecomputedChunk>,
138
139 + // external runtime script chunks
140 + externalRuntimeScript: null | ExternalRuntimeScript,
141 + bootstrapChunks: Array<Chunk | PrecomputedChunk>,
142 +
143 // Hoistable chunks
144 charsetChunks: Array<Chunk | PrecomputedChunk>,
145 preconnectChunks: Array<Chunk | PrecomputedChunk>,
@@ -142,6 +147,20 @@ export type RenderState = {
147 preloadChunks: Array<Chunk | PrecomputedChunk>,
148 hoistableChunks: Array<Chunk | PrecomputedChunk>,
149
150 + // Flushing queues for Resource dependencies
151 + preconnects: Set<PreconnectResource>,
152 + fontPreloads: Set<PreloadResource>,
153 + highImagePreloads: Set<PreloadResource>,
154 + // usedImagePreloads: Set<PreloadResource>,
155 + precedences: Map<string, Map<string, StyleResource>>,
156 + stylePrecedences: Map<string, StyleTagResource>,
157 + bootstrapScripts: Set<PreloadResource>,
158 + scripts: Set<ScriptResource>,
159 + bulkPreloads: Set<PreloadResource>,
160 +
161 + // Temporarily keeps track of key to preload resources before shell flushes.
162 + preloadsMap: Map<string, PreloadResource>,
163 +
164 // Module-global-like reference for current boundary resources
165 boundaryResources: ?BoundaryResources,
166
@@ -158,9 +177,6 @@ export type RenderState = {
177 // Per response, global state that is not contextual to the rendering subtree.
178 // This is resumable and therefore should be serializable.
179 export type ResumableState = {
161 - // external runtime script chunks
162 - externalRuntimeScript: null | ExternalRuntimeScript, // TODO: Move to a serializable format
163 - bootstrapChunks: Array<Chunk | PrecomputedChunk>, // TODO: Move to a serializable format.
180 idPrefix: string,
181 nextFormID: number,
182 streamingFormat: StreamingFormat,
@@ -172,24 +188,11 @@ export type ResumableState = {
188 hasBody: boolean,
189 hasHtml: boolean,
190
175 - // Resources
176 -
177 - // Request local cache
178 - preloadsMap: Map<string, PreloadResource>,
179 - preconnectsMap: Map<string, PreconnectResource>,
180 - stylesMap: Map<string, StyleResource>,
181 - scriptsMap: Map<string, ScriptResource>,
182 -
183 - // Flushing queues for Resource dependencies
184 - preconnects: Set<PreconnectResource>,
185 - fontPreloads: Set<PreloadResource>,
186 - highImagePreloads: Set<PreloadResource>,
187 - // usedImagePreloads: Set<PreloadResource>,
188 - precedences: Map<string, Set<StyleResource>>,
189 - stylePrecedences: Map<string, StyleTagResource>,
190 - bootstrapScripts: Set<PreloadResource>,
191 - scripts: Set<ScriptResource>,
192 - bulkPreloads: Set<PreloadResource>,
191 + // Resources - Request local cache
192 + preloadsMap: {[key: string]: PreloadProps},
193 + preconnectsMap: {[key: string]: null},
194 + stylesMap: {[key: string]: null},
195 + scriptsMap: {[key: string]: null},
196 };
197
198 const dataElementQuotedEnd = stringToPrecomputedChunk('"></template>');
@@ -249,6 +252,10 @@ const importMapScriptEnd = stringToPrecomputedChunk('</script>');
252 export function createRenderState(
253 resumableState: ResumableState,
254 nonce: string | void,
255 + bootstrapScriptContent: string | void,
256 + bootstrapScripts: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,
257 + bootstrapModules: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,
258 + externalRuntimeConfig: string | BootstrapScriptDescriptor | void,
259 importMap: ImportMap | void,
260 ): RenderState {
261 const inlineScriptWithNonce =
@@ -258,55 +265,10 @@ export function createRenderState(
265 '<script nonce="' + escapeTextForBrowser(nonce) + '">',
266 );
267 const idPrefix = resumableState.idPrefix;
261 - const importMapChunks: Array<Chunk | PrecomputedChunk> = [];
262 - if (importMap !== undefined) {
263 - const map = importMap;
264 - importMapChunks.push(importMapScriptStart);
265 - importMapChunks.push(
266 - stringToChunk(
267 - escapeBootstrapAndImportMapScriptContent(JSON.stringify(map)),
268 - ),
269 - );
270 - importMapChunks.push(importMapScriptEnd);
271 - }
272 - return {
273 - placeholderPrefix: stringToPrecomputedChunk(idPrefix + 'P:'),
274 - segmentPrefix: stringToPrecomputedChunk(idPrefix + 'S:'),
275 - boundaryPrefix: stringToPrecomputedChunk(idPrefix + 'B:'),
276 - startInlineScript: inlineScriptWithNonce,
277 - htmlChunks: null,
278 - headChunks: null,
279 - charsetChunks: [],
280 - preconnectChunks: [],
281 - importMapChunks,
282 - preloadChunks: [],
283 - hoistableChunks: [],
284 - nonce,
285 - // like a module global for currently rendering boundary
286 - boundaryResources: null,
287 - stylesToHoist: false,
288 - };
289 -}
268
291 -export function createResumableState(
292 - identifierPrefix: string | void,
293 - nonce: string | void,
294 - bootstrapScriptContent: string | void,
295 - bootstrapScripts: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,
296 - bootstrapModules: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,
297 - externalRuntimeConfig: string | BootstrapScriptDescriptor | void,
298 -): ResumableState {
299 - const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix;
269 const bootstrapChunks: Array<Chunk | PrecomputedChunk> = [];
301 - let streamingFormat = ScriptStreamingFormat;
270 let externalRuntimeScript: null | ExternalRuntimeScript = null;
271 if (bootstrapScriptContent !== undefined) {
304 - const inlineScriptWithNonce =
305 - nonce === undefined
306 - ? startInlineScript
307 - : stringToPrecomputedChunk(
308 - '<script nonce="' + escapeTextForBrowser(nonce) + '">',
309 - );
272 bootstrapChunks.push(
273 inlineScriptWithNonce,
274 stringToChunk(
@@ -322,7 +284,6 @@ export function createResumableState(
284 );
285 }
286 if (externalRuntimeConfig !== undefined) {
325 - streamingFormat = DataStreamingFormat;
287 if (typeof externalRuntimeConfig === 'string') {
288 externalRuntimeScript = {
289 src: externalRuntimeConfig,
@@ -349,24 +310,33 @@ export function createResumableState(
310 }
311 }
312
352 - const resumableState: ResumableState = {
313 + const importMapChunks: Array<Chunk | PrecomputedChunk> = [];
314 + if (importMap !== undefined) {
315 + const map = importMap;
316 + importMapChunks.push(importMapScriptStart);
317 + importMapChunks.push(
318 + stringToChunk(
319 + escapeBootstrapAndImportMapScriptContent(JSON.stringify(map)),
320 + ),
321 + );
322 + importMapChunks.push(importMapScriptEnd);
323 + }
324 + const renderState: RenderState = {
325 + placeholderPrefix: stringToPrecomputedChunk(idPrefix + 'P:'),
326 + segmentPrefix: stringToPrecomputedChunk(idPrefix + 'S:'),
327 + boundaryPrefix: stringToPrecomputedChunk(idPrefix + 'B:'),
328 + startInlineScript: inlineScriptWithNonce,
329 + htmlChunks: null,
330 + headChunks: null,
331 +
332 externalRuntimeScript: externalRuntimeScript,
333 bootstrapChunks: bootstrapChunks,
355 - idPrefix: idPrefix,
356 - nextFormID: 0,
357 - streamingFormat,
358 - instructions: NothingSent,
359 - hasBody: false,
360 - hasHtml: false,
361 -
362 - // @TODO add bootstrap script to implicit preloads
363 -
364 - // persistent
365 - preloadsMap: new Map(),
366 - preconnectsMap: new Map(),
367 - stylesMap: new Map(),
368 - scriptsMap: new Map(),
334
335 + charsetChunks: [],
336 + preconnectChunks: [],
337 + importMapChunks,
338 + preloadChunks: [],
339 + hoistableChunks: [],
340 // cleared on flush
341 preconnects: new Set(),
342 fontPreloads: new Set(),
@@ -377,6 +347,13 @@ export function createResumableState(
347 bootstrapScripts: new Set(),
348 scripts: new Set(),
349 bulkPreloads: new Set(),
350 +
351 + preloadsMap: new Map(),
352 +
353 + nonce,
354 + // like a module global for currently rendering boundary
355 + boundaryResources: null,
356 + stylesToHoist: false,
357 };
358
359 if (bootstrapScripts !== undefined) {
@@ -395,6 +372,7 @@ export function createResumableState(
372
373 preloadBootstrapScript(
374 resumableState,
375 + renderState,
376 src,
377 nonce,
378 integrity,
@@ -442,6 +420,7 @@ export function createResumableState(
420
421 preloadBootstrapModule(
422 resumableState,
423 + renderState,
424 src,
425 nonce,
426 integrity,
@@ -475,7 +454,53 @@ export function createResumableState(
454 }
455 }
456
478 - return resumableState;
457 + return renderState;
458 +}
459 +
460 +export function resumeRenderState(
461 + resumableState: ResumableState,
462 + nonce: string | void,
463 +): RenderState {
464 + return createRenderState(
465 + resumableState,
466 + nonce,
467 + // These should have already been flushed in the prerender.
468 + undefined,
469 + undefined,
470 + undefined,
471 + undefined,
472 + undefined,
473 + );
474 +}
475 +
476 +export function createResumableState(
477 + identifierPrefix: string | void,
478 + externalRuntimeConfig: string | BootstrapScriptDescriptor | void,
479 +): ResumableState {
480 + const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix;
481 +
482 + let streamingFormat = ScriptStreamingFormat;
483 + if (enableFizzExternalRuntime) {
484 + if (externalRuntimeConfig !== undefined) {
485 + streamingFormat = DataStreamingFormat;
486 + }
487 + }
488 + return {
489 + idPrefix: idPrefix,
490 + nextFormID: 0,
491 + streamingFormat,
492 + instructions: NothingSent,
493 + hasBody: false,
494 + hasHtml: false,
495 +
496 + // @TODO add bootstrap script to implicit preloads
497 +
498 + // persistent
499 + preloadsMap: {},
500 + preconnectsMap: {},
501 + stylesMap: {},
502 + scriptsMap: {},
503 + };
504 }
505
506 // Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion
@@ -1490,10 +1515,10 @@ function injectFormReplayingRuntime(
1515 // to emit anything. It's always used.
1516 if (
1517 (resumableState.instructions & SentFormReplayingRuntime) === NothingSent &&
1493 - (!enableFizzExternalRuntime || !resumableState.externalRuntimeScript)
1518 + (!enableFizzExternalRuntime || !renderState.externalRuntimeScript)
1519 ) {
1520 resumableState.instructions |= SentFormReplayingRuntime;
1496 - resumableState.bootstrapChunks.unshift(
1521 + renderState.bootstrapChunks.unshift(
1522 renderState.startInlineScript,
1523 formReplayingRuntimeScript,
1524 endInlineScript,
@@ -2111,34 +2136,37 @@ function pushLink(
2136 return pushLinkImpl(target, props);
2137 } else {
2138 // This stylesheet refers to a Resource and we create a new one if necessary
2114 - let resource = resumableState.stylesMap.get(key);
2115 - if (!resource) {
2139 + let stylesInPrecedence = renderState.precedences.get(precedence);
2140 + if (!resumableState.stylesMap.hasOwnProperty(key)) {
2141 const resourceProps = stylesheetPropsFromRawProps(props);
2117 - const preloadResource = resumableState.preloadsMap.get(key);
2142 let state = NoState;
2119 - if (preloadResource) {
2120 - // If we already had a preload we don't want that resource to flush directly.
2121 - // We let the newly created resource govern flushing.
2122 - preloadResource.state |= Blocked;
2123 - adoptPreloadPropsForStylesheetProps(
2124 - resourceProps,
2125 - preloadResource.props,
2126 - );
2127 - if (preloadResource.state & Flushed) {
2143 + if (resumableState.preloadsMap.hasOwnProperty(key)) {
2144 + const preloadProps: PreloadProps = resumableState.preloadsMap[key];
2145 + adoptPreloadPropsForStylesheetProps(resourceProps, preloadProps);
2146 + const preloadResource = renderState.preloadsMap.get(key);
2147 + if (preloadResource) {
2148 + // If we already had a preload we don't want that resource to flush directly.
2149 + // We let the newly created resource govern flushing.
2150 + preloadResource.state |= Blocked;
2151 + if (preloadResource.state & Flushed) {
2152 + state = PreloadFlushed;
2153 + }
2154 + } else {
2155 + // If we resumed then we assume that this was already flushed
2156 + // by the shell.
2157 state = PreloadFlushed;
2158 }
2159 }
2131 - resource = {
2160 + const resource = {
2161 type: 'stylesheet',
2162 chunks: ([]: Array<Chunk | PrecomputedChunk>),
2163 state,
2164 props: resourceProps,
2165 };
2137 - resumableState.stylesMap.set(key, resource);
2138 - let precedenceSet = resumableState.precedences.get(precedence);
2139 - if (!precedenceSet) {
2140 - precedenceSet = new Set();
2141 - resumableState.precedences.set(precedence, precedenceSet);
2166 + resumableState.stylesMap[key] = null;
2167 + if (!stylesInPrecedence) {
2168 + stylesInPrecedence = new Map();
2169 + renderState.precedences.set(precedence, stylesInPrecedence);
2170 const emptyStyleResource = {
2171 type: 'style',
2172 chunks: ([]: Array<Chunk | PrecomputedChunk>),
@@ -2148,21 +2176,35 @@ function pushLink(
2176 hrefs: ([]: Array<string>),
2177 },
2178 };
2151 - precedenceSet.add(emptyStyleResource);
2179 + stylesInPrecedence.set('', emptyStyleResource);
2180 if (__DEV__) {
2153 - if (resumableState.stylePrecedences.has(precedence)) {
2181 + if (renderState.stylePrecedences.has(precedence)) {
2182 console.error(
2183 'React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.',
2184 precedence,
2185 );
2186 }
2187 }
2160 - resumableState.stylePrecedences.set(precedence, emptyStyleResource);
2188 + renderState.stylePrecedences.set(precedence, emptyStyleResource);
2189 + }
2190 + stylesInPrecedence.set(key, resource);
2191 + if (renderState.boundaryResources) {
2192 + renderState.boundaryResources.add(resource);
2193 + }
2194 + } else {
2195 + // We need to track whether this boundary should wait on this resource or not.
2196 + // Typically this resource should always exist since we either had it or just created
2197 + // it. However, it's possible when you resume that the style has already been emitted
2198 + // and then it wouldn't be recreated in the RenderState and there's no need to track
2199 + // it again since we should've hoisted it to the shell already.
2200 + if (stylesInPrecedence) {
2201 + const resource = stylesInPrecedence.get(key);
2202 + if (resource) {
2203 + if (renderState.boundaryResources) {
2204 + renderState.boundaryResources.add(resource);
2205 + }
2206 + }
2207 }
2162 - precedenceSet.add(resource);
2163 - }
2164 - if (renderState.boundaryResources) {
2165 - renderState.boundaryResources.add(resource);
2208 }
2209 if (textEmbedded) {
2210 // This link follows text but we aren't writing a tag. while not as efficient as possible we need
@@ -2293,9 +2335,8 @@ function pushStyle(
2335 }
2336
2337 const key = getResourceKey('style', href);
2296 - let resource = resumableState.stylesMap.get(key);
2297 - if (!resource) {
2298 - resource = resumableState.stylePrecedences.get(precedence);
2338 + let resource = renderState.stylePrecedences.get(precedence);
2339 + if (!resumableState.stylesMap.hasOwnProperty(key)) {
2340 if (!resource) {
2341 resource = {
2342 type: 'style',
@@ -2306,26 +2347,33 @@ function pushStyle(
2347 hrefs: [href],
2348 },
2349 };
2309 - resumableState.stylePrecedences.set(precedence, resource);
2310 - const precedenceSet: Set<StyleResource> = new Set();
2311 - precedenceSet.add(resource);
2350 + renderState.stylePrecedences.set(precedence, resource);
2351 + const stylesInPrecedence: Map<string, StyleResource> = new Map();
2352 + stylesInPrecedence.set('', resource);
2353 if (__DEV__) {
2313 - if (resumableState.precedences.has(precedence)) {
2354 + if (renderState.precedences.has(precedence)) {
2355 console.error(
2356 'React constructed a new style precedence set when one already exists for this precedence: "%s". This is a bug in React.',
2357 precedence,
2358 );
2359 }
2360 }
2320 - resumableState.precedences.set(precedence, precedenceSet);
2361 + renderState.precedences.set(precedence, stylesInPrecedence);
2362 } else {
2363 resource.props.hrefs.push(href);
2364 }
2324 - resumableState.stylesMap.set(key, resource);
2365 + resumableState.stylesMap[key] = null;
2366 + pushStyleContents(resource.chunks, props);
2367 + }
2368 + if (resource) {
2369 + // We need to track whether this boundary should wait on this resource or not.
2370 + // Typically this resource should always exist since we either had it or just created
2371 + // it. However, it's possible when you resume that the style has already been emitted
2372 + // and then it wouldn't be recreated in the RenderState and there's no need to track
2373 + // it again since we should've hoisted it to the shell already.
2374 if (renderState.boundaryResources) {
2375 renderState.boundaryResources.add(resource);
2376 }
2328 - pushStyleContents(resource.chunks, props);
2377 }
2378
2379 if (textEmbedded) {
@@ -2448,6 +2496,7 @@ function pushImg(
2496 target: Array<Chunk | PrecomputedChunk>,
2497 props: Object,
2498 resumableState: ResumableState,
2499 + renderState: RenderState,
2500 pictureTagInScope: boolean,
2501 ): null {
2502 const {src, srcSet} = props;
@@ -2478,39 +2527,45 @@ function pushImg(
2527 // resumableState.
2528 const {sizes} = props;
2529 const key = getImagePreloadKey(src, srcSet, sizes);
2481 - let resource = resumableState.preloadsMap.get(key);
2482 - if (!resource) {
2530 + let resource: void | PreloadResource;
2531 + if (!resumableState.preloadsMap.hasOwnProperty(key)) {
2532 + const preloadProps: PreloadProps = {
2533 + rel: 'preload',
2534 + as: 'image',
2535 + // There is a bug in Safari where imageSrcSet is not respected on preload links
2536 + // so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
2537 + // This harms older browers that do not support imageSrcSet by making their preloads not work
2538 + // but this population is shrinking fast and is already small so we accept this tradeoff.
2539 + href: srcSet ? undefined : src,
2540 + imageSrcSet: srcSet,
2541 + imageSizes: sizes,
2542 + crossOrigin: props.crossOrigin,
2543 + integrity: props.integrity,
2544 + type: props.type,
2545 + fetchPriority: props.fetchPriority,
2546 + referrerPolicy: props.referrerPolicy,
2547 + };
2548 resource = {
2549 type: 'preload',
2550 chunks: [],
2551 state: NoState,
2487 - props: {
2488 - rel: 'preload',
2489 - as: 'image',
2490 - // There is a bug in Safari where imageSrcSet is not respected on preload links
2491 - // so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
2492 - // This harms older browers that do not support imageSrcSet by making their preloads not work
2493 - // but this population is shrinking fast and is already small so we accept this tradeoff.
2494 - href: srcSet ? undefined : src,
2495 - imageSrcSet: srcSet,
2496 - imageSizes: sizes,
2497 - crossOrigin: props.crossOrigin,
2498 - integrity: props.integrity,
2499 - type: props.type,
2500 - fetchPriority: props.fetchPriority,
2501 - referrerPolicy: props.referrerPolicy,
2502 - },
2552 + props: preloadProps,
2553 };
2504 - resumableState.preloadsMap.set(key, resource);
2505 - pushLinkImpl(resource.chunks, resource.props);
2506 - }
2507 - if (
2508 - props.fetchPriority === 'high' ||
2509 - resumableState.highImagePreloads.size < 10
2510 - ) {
2511 - resumableState.highImagePreloads.add(resource);
2554 + resumableState.preloadsMap[key] = preloadProps;
2555 + renderState.preloadsMap.set(key, resource);
2556 + pushLinkImpl(resource.chunks, preloadProps);
2557 } else {
2513 - resumableState.bulkPreloads.add(resource);
2558 + resource = renderState.preloadsMap.get(key);
2559 + }
2560 + if (resource) {
2561 + if (
2562 + props.fetchPriority === 'high' ||
2563 + renderState.highImagePreloads.size < 10
2564 + ) {
2565 + renderState.highImagePreloads.add(resource);
2566 + } else {
2567 + renderState.bulkPreloads.add(resource);
2568 + }
2569 }
2570 }
2571 return pushSelfClosing(target, props, 'img');
@@ -2818,6 +2873,7 @@ function pushScript(
2873 target: Array<Chunk | PrecomputedChunk>,
2874 props: Object,
2875 resumableState: ResumableState,
2876 + renderState: RenderState,
2877 textEmbedded: boolean,
2878 insertionMode: InsertionMode,
2879 noscriptTagInScope: boolean,
@@ -2845,26 +2901,28 @@ function pushScript(
2901 const src = props.src;
2902 const key = getResourceKey('script', src);
2903 // We can make this <script> into a ScriptResource
2848 - let resource = resumableState.scriptsMap.get(key);
2849 - if (!resource) {
2850 - resource = {
2904 + if (!resumableState.scriptsMap.hasOwnProperty(key)) {
2905 + const resource: ScriptResource = {
2906 type: 'script',
2907 chunks: [],
2908 state: NoState,
2909 props: null,
2910 };
2856 - resumableState.scriptsMap.set(key, resource);
2911 + resumableState.scriptsMap[key] = null;
2912 // Add to the script flushing queue
2858 - resumableState.scripts.add(resource);
2913 + renderState.scripts.add(resource);
2914
2915 let scriptProps = props;
2861 - const preloadResource = resumableState.preloadsMap.get(key);
2862 - if (preloadResource) {
2863 - // If we already had a preload we don't want that resource to flush directly.
2864 - // We let the newly created resource govern flushing.
2865 - preloadResource.state |= Blocked;
2916 + if (resumableState.preloadsMap.hasOwnProperty(key)) {
2917 + const preloadProps: PreloadProps = resumableState.preloadsMap[key];
2918 scriptProps = {...props};
2867 - adoptPreloadPropsForScriptProps(scriptProps, preloadResource.props);
2919 + adoptPreloadPropsForScriptProps(scriptProps, preloadProps);
2920 + const preloadResource = renderState.preloadsMap.get(key);
2921 + if (preloadResource) {
2922 + // If we already had a preload we don't want that resource to flush directly.
2923 + // We let the newly created resource govern flushing.
2924 + preloadResource.state |= Blocked;
2925 + }
2926 }
2927 // encode the tag as Chunks
2928 pushScriptImpl(resource.chunks, scriptProps);
@@ -3242,6 +3300,7 @@ export function pushStartInstance(
3300 target,
3301 props,
3302 resumableState,
3303 + renderState,
3304 textEmbedded,
3305 formatContext.insertionMode,
3306 !!(formatContext.tagScope & NOSCRIPT_SCOPE),
@@ -3277,6 +3336,7 @@ export function pushStartInstance(
3336 target,
3337 props,
3338 resumableState,
3339 + renderState,
3340 !!(formatContext.tagScope & PICTURE_SCOPE),
3341 )
3342 : pushSelfClosing(target, props, type);
@@ -3402,9 +3462,9 @@ export function pushEndInstance(
3462
3463 function writeBootstrap(
3464 destination: Destination,
3405 - resumableState: ResumableState,
3465 + renderState: RenderState,
3466 ): boolean {
3407 - const bootstrapChunks = resumableState.bootstrapChunks;
3467 + const bootstrapChunks = renderState.bootstrapChunks;
3468 let i = 0;
3469 for (; i < bootstrapChunks.length - 1; i++) {
3470 writeChunk(destination, bootstrapChunks[i]);
@@ -3419,9 +3479,9 @@ function writeBootstrap(
3479
3480 export function writeCompletedRoot(
3481 destination: Destination,
3422 - resumableState: ResumableState,
3482 + renderState: RenderState,
3483 ): boolean {
3424 - return writeBootstrap(destination, resumableState);
3484 + return writeBootstrap(destination, renderState);
3485 }
3486
3487 // Structural Nodes
@@ -3886,7 +3946,7 @@ export function writeCompletedBoundaryInstruction(
3946 } else {
3947 writeMore = writeChunkAndReturn(destination, completeBoundaryDataEnd);
3948 }
3889 - return writeBootstrap(destination, resumableState) && writeMore;
3949 + return writeBootstrap(destination, renderState) && writeMore;
3950 }
3951
3952 const clientRenderScript1Full = stringToPrecomputedChunk(
@@ -4166,15 +4226,15 @@ let didFlushPrecedence = false;
4226 function flushStyleInPreamble(
4227 this: Destination,
4228 resource: StyleResource,
4169 - key: mixed,
4170 - set: Set<StyleResource>,
4229 + key: string,
4230 + map: Map<string, StyleResource>,
4231 ) {
4232 const chunks = resource.chunks;
4233 if (resource.state & Flushed) {
4234 // In theory this should never happen because we clear from the
4235 // Set on flush but to ensure correct semantics we don't emit
4236 // anything if we are in this state.
4177 - set.delete(resource);
4237 + map.delete(key);
4238 } else {
4239 // We can emit this style or stylesheet as is.
4240 if (resource.type === 'style') {
@@ -4206,12 +4266,12 @@ const styleTagResourceClose = stringToPrecomputedChunk('</style>');
4266
4267 function flushAllStylesInPreamble(
4268 this: Destination,
4209 - set: Set<StyleResource>,
4269 + map: Map<string, StyleResource>,
4270 precedence: string,
4271 ) {
4272 didFlushPrecedence = false;
4213 - set.forEach(flushStyleInPreamble, this);
4214 - set.clear();
4273 + map.forEach(flushStyleInPreamble, this);
4274 + map.clear();
4275
4276 const chunks = precedenceStyleTagResource.chunks;
4277 const hrefs = precedenceStyleTagResource.props.hrefs;
@@ -4267,11 +4327,11 @@ function preloadLateStyle(this: Destination, resource: StyleResource) {
4327
4328 function preloadLateStyles(
4329 this: Destination,
4270 - set: Set<StyleResource>,
4330 + map: Map<string, StyleResource>,
4331 precedence: string,
4332 ) {
4273 - set.forEach(preloadLateStyle, this);
4274 - set.clear();
4333 + map.forEach(preloadLateStyle, this);
4334 + map.clear();
4335 }
4336
4337 // We don't bother reporting backpressure at the moment because we expect to
@@ -4288,15 +4348,15 @@ export function writePreamble(
4348 if (
4349 enableFizzExternalRuntime &&
4350 !willFlushAllSegments &&
4291 - resumableState.externalRuntimeScript
4351 + renderState.externalRuntimeScript
4352 ) {
4353 // If the root segment is incomplete due to suspended tasks
4354 // (e.g. willFlushAllSegments = false) and we are using data
4355 // streaming format, ensure the external runtime is sent.
4356 // (User code could choose to send this even earlier by calling
4357 // preinit(...), if they know they will suspend).
4298 - const {src, chunks} = resumableState.externalRuntimeScript;
4299 - internalPreinitScript(resumableState, src, chunks);
4358 + const {src, chunks} = renderState.externalRuntimeScript;
4359 + internalPreinitScript(resumableState, renderState, src, chunks);
4360 }
4361
4362 const htmlChunks = renderState.htmlChunks;
@@ -4334,8 +4394,8 @@ export function writePreamble(
4394 charsetChunks.length = 0;
4395
4396 // emit preconnect resources
4337 - resumableState.preconnects.forEach(flushResourceInPreamble, destination);
4338 - resumableState.preconnects.clear();
4397 + renderState.preconnects.forEach(flushResourceInPreamble, destination);
4398 + renderState.preconnects.clear();
4399
4400 const preconnectChunks = renderState.preconnectChunks;
4401 for (i = 0; i < preconnectChunks.length; i++) {
@@ -4343,17 +4403,14 @@ export function writePreamble(
4403 }
4404 preconnectChunks.length = 0;
4405
4346 - resumableState.fontPreloads.forEach(flushResourceInPreamble, destination);
4347 - resumableState.fontPreloads.clear();
4406 + renderState.fontPreloads.forEach(flushResourceInPreamble, destination);
4407 + renderState.fontPreloads.clear();
4408
4349 - resumableState.highImagePreloads.forEach(
4350 - flushResourceInPreamble,
4351 - destination,
4352 - );
4353 - resumableState.highImagePreloads.clear();
4409 + renderState.highImagePreloads.forEach(flushResourceInPreamble, destination);
4410 + renderState.highImagePreloads.clear();
4411
4412 // Flush unblocked stylesheets by precedence
4356 - resumableState.precedences.forEach(flushAllStylesInPreamble, destination);
4413 + renderState.precedences.forEach(flushAllStylesInPreamble, destination);
4414
4415 const importMapChunks = renderState.importMapChunks;
4416 for (i = 0; i < importMapChunks.length; i++) {
@@ -4361,13 +4418,13 @@ export function writePreamble(
4418 }
4419 importMapChunks.length = 0;
4420
4364 - resumableState.bootstrapScripts.forEach(flushResourceInPreamble, destination);
4421 + renderState.bootstrapScripts.forEach(flushResourceInPreamble, destination);
4422
4366 - resumableState.scripts.forEach(flushResourceInPreamble, destination);
4367 - resumableState.scripts.clear();
4423 + renderState.scripts.forEach(flushResourceInPreamble, destination);
4424 + renderState.scripts.clear();
4425
4369 - resumableState.bulkPreloads.forEach(flushResourceInPreamble, destination);
4370 - resumableState.bulkPreloads.clear();
4426 + renderState.bulkPreloads.forEach(flushResourceInPreamble, destination);
4427 + renderState.bulkPreloads.clear();
4428
4429 // Write embedding preloadChunks
4430 const preloadChunks = renderState.preloadChunks;
@@ -4412,8 +4469,8 @@ export function writeHoistables(
4469 // We omit charsetChunks because we have already sent the shell and if it wasn't
4470 // already sent it is too late now.
4471
4415 - resumableState.preconnects.forEach(flushResourceLate, destination);
4416 - resumableState.preconnects.clear();
4472 + renderState.preconnects.forEach(flushResourceLate, destination);
4473 + renderState.preconnects.clear();
4474
4475 const preconnectChunks = renderState.preconnectChunks;
4476 for (i = 0; i < preconnectChunks.length; i++) {
@@ -4421,18 +4478,15 @@ export function writeHoistables(
4478 }
4479 preconnectChunks.length = 0;
4480
4424 - resumableState.fontPreloads.forEach(flushResourceLate, destination);
4425 - resumableState.fontPreloads.clear();
4481 + renderState.fontPreloads.forEach(flushResourceLate, destination);
4482 + renderState.fontPreloads.clear();
4483
4427 - resumableState.highImagePreloads.forEach(
4428 - flushResourceInPreamble,
4429 - destination,
4430 - );
4431 - resumableState.highImagePreloads.clear();
4484 + renderState.highImagePreloads.forEach(flushResourceInPreamble, destination);
4485 + renderState.highImagePreloads.clear();
4486
4487 // Preload any stylesheets. these will emit in a render instruction that follows this
4488 // but we want to kick off preloading as soon as possible
4435 - resumableState.precedences.forEach(preloadLateStyles, destination);
4489 + renderState.precedences.forEach(preloadLateStyles, destination);
4490
4491 // We only hoist importmaps that are configured through createResponse and that will
4492 // always flush in the preamble. Generally we don't expect people to render them as
@@ -4442,11 +4496,11 @@ export function writeHoistables(
4496 // bootstrap scripts should flush above script priority but these can only flush in the preamble
4497 // so we elide the code here for performance
4498
4445 - resumableState.scripts.forEach(flushResourceLate, destination);
4446 - resumableState.scripts.clear();
4499 + renderState.scripts.forEach(flushResourceLate, destination);
4500 + renderState.scripts.clear();
4501
4448 - resumableState.bulkPreloads.forEach(flushResourceLate, destination);
4449 - resumableState.bulkPreloads.clear();
4502 + renderState.bulkPreloads.forEach(flushResourceLate, destination);
4503 + renderState.bulkPreloads.clear();
4504
4505 // Write embedding preloadChunks
4506 const preloadChunks = renderState.preloadChunks;
@@ -4913,12 +4967,12 @@ type PreloadAsProps = {
4967 rel: 'preload',
4968 as: string,
4969 href: ?string,
4916 - [string]: mixed,
4970 + [string]: ?string,
4971 };
4972 type PreloadModuleProps = {
4973 rel: 'modulepreload',
4974 href: ?string,
4921 - [string]: mixed,
4975 + [string]: ?string,
4976 };
4977 type PreloadProps = PreloadAsProps | PreloadModuleProps;
4978 type PreloadResource = TResource<'preload', PreloadProps>;
@@ -4989,24 +5043,24 @@ function prefetchDNS(href: string) {
5043 return;
5044 }
5045 const resumableState = getResumableState(request);
5046 + const renderState = getRenderState(request);
5047
5048 if (typeof href === 'string' && href) {
5049 const key = getResourceKey('prefetchDNS', href);
4995 - let resource = resumableState.preconnectsMap.get(key);
4996 - if (!resource) {
4997 - resource = {
5050 + if (!resumableState.preconnectsMap.hasOwnProperty(key)) {
5051 + const resource: PreconnectResource = {
5052 type: 'preconnect',
5053 chunks: [],
5054 state: NoState,
5055 props: null,
5056 };
5003 - resumableState.preconnectsMap.set(key, resource);
5057 + resumableState.preconnectsMap[key] = null;
5058 pushLinkImpl(
5059 resource.chunks,
5060 ({href, rel: 'dns-prefetch'}: PreconnectProps),
5061 );
5062 + renderState.preconnects.add(resource);
5063 }
5009 - resumableState.preconnects.add(resource);
5064 flushResources(request);
5065 }
5066 }
@@ -5025,26 +5079,26 @@ function preconnect(href: string, crossOrigin: ?CrossOriginEnum) {
5079 return;
5080 }
5081 const resumableState = getResumableState(request);
5082 + const renderState = getRenderState(request);
5083
5084 if (typeof href === 'string' && href) {
5085 const key = `[preconnect][${
5086 typeof crossOrigin === 'string' ? crossOrigin : 'null'
5087 }]${href}`;
5033 - let resource = resumableState.preconnectsMap.get(key);
5034 - if (!resource) {
5035 - resource = {
5088 + if (!resumableState.preconnectsMap.hasOwnProperty(key)) {
5089 + const resource: PreconnectResource = {
5090 type: 'preconnect',
5091 chunks: [],
5092 state: NoState,
5093 props: null,
5094 };
5041 - resumableState.preconnectsMap.set(key, resource);
5095 + resumableState.preconnectsMap[key] = null;
5096 pushLinkImpl(
5097 resource.chunks,
5098 ({rel: 'preconnect', href, crossOrigin}: PreconnectProps),
5099 );
5100 + renderState.preconnects.add(resource);
5101 }
5047 - resumableState.preconnects.add(resource);
5102 flushResources(request);
5103 }
5104 }
@@ -5063,6 +5117,7 @@ function preload(href: string, as: string, options?: ?PreloadImplOptions) {
5117 return;
5118 }
5119 const resumableState = getResumableState(request);
5120 + const renderState = getRenderState(request);
5121 if (as && href) {
5122 options = options || {};
5123 let key: string;
@@ -5074,8 +5129,7 @@ function preload(href: string, as: string, options?: ?PreloadImplOptions) {
5129 } else {
5130 key = getResourceKey(as, href);
5131 }
5077 - let resource = resumableState.preloadsMap.get(key);
5078 - if (!resource) {
5132 + if (!resumableState.preloadsMap.hasOwnProperty(key)) {
5133 const props = Object.assign(
5134 ({
5135 rel: 'preload',
@@ -5084,21 +5138,22 @@ function preload(href: string, as: string, options?: ?PreloadImplOptions) {
5138 }: PreloadProps),
5139 options,
5140 );
5087 - resource = {
5141 + const resource: PreloadResource = {
5142 type: 'preload',
5143 chunks: [],
5144 state: NoState,
5145 props,
5146 };
5093 - resumableState.preloadsMap.set(key, resource);
5094 - pushLinkImpl(resource.chunks, resource.props);
5095 - }
5096 - if (as === 'font') {
5097 - resumableState.fontPreloads.add(resource);
5098 - } else if (as === 'image' && resource.props.fetchPriority === 'high') {
5099 - resumableState.highImagePreloads.add(resource);
5100 - } else {
5101 - resumableState.bulkPreloads.add(resource);
5147 + resumableState.preloadsMap[key] = props;
5148 + renderState.preloadsMap.set(key, resource);
5149 + pushLinkImpl(resource.chunks, props);
5150 + if (as === 'font') {
5151 + renderState.fontPreloads.add(resource);
5152 + } else if (as === 'image' && resource.props.fetchPriority === 'high') {
5153 + renderState.highImagePreloads.add(resource);
5154 + } else {
5155 + renderState.bulkPreloads.add(resource);
5156 + }
5157 }
5158 flushResources(request);
5159 }
@@ -5121,29 +5176,30 @@ function preloadModule(
5176 return;
5177 }
5178 const resumableState = getResumableState(request);
5179 + const renderState = getRenderState(request);
5180 if (href) {
5181 const as =
5182 options && typeof options.as === 'string' ? options.as : 'script';
5183 const key = getResourceKey(as, href);
5128 - let resource = resumableState.preloadsMap.get(key);
5129 - const props: PreloadModuleProps = Object.assign(
5130 - ({
5131 - rel: 'modulepreload',
5132 - href,
5133 - }: PreloadModuleProps),
5134 - options,
5135 - );
5136 - if (!resource) {
5137 - resource = {
5184 + if (!resumableState.preloadsMap.hasOwnProperty(key)) {
5185 + const props: PreloadModuleProps = Object.assign(
5186 + ({
5187 + rel: 'modulepreload',
5188 + href,
5189 + }: PreloadModuleProps),
5190 + options,
5191 + );
5192 + const resource: PreloadResource = {
5193 type: 'preload',
5194 chunks: [],
5195 state: NoState,
5196 props,
5197 };
5143 - resumableState.preloadsMap.set(key, resource);
5198 + resumableState.preloadsMap[key] = props;
5199 + renderState.preloadsMap.set(key, resource);
5200 pushLinkImpl(resource.chunks, resource.props);
5201 + renderState.bulkPreloads.add(resource);
5202 }
5146 - resumableState.bulkPreloads.add(resource);
5203 flushResources(request);
5204 }
5205 }
@@ -5166,16 +5222,20 @@ function preinitStyle(
5222 return;
5223 }
5224 const resumableState = getResumableState(request);
5225 + const renderState = getRenderState(request);
5226 if (href) {
5227 const as = 'style';
5228 const key = getResourceKey(as, href);
5172 - let resource = resumableState.stylesMap.get(key);
5173 - if (!resource) {
5229 + if (!resumableState.stylesMap.hasOwnProperty(key)) {
5230 precedence = precedence || 'default';
5231 let state = NoState;
5176 - const preloadResource = resumableState.preloadsMap.get(key);
5232 + const preloadResource = renderState.preloadsMap.get(key);
5233 if (preloadResource && preloadResource.state & Flushed) {
5234 state = PreloadFlushed;
5235 + } else if (resumableState.preloadsMap.hasOwnProperty(key)) {
5236 + // If we resumed then we assume that this was already flushed
5237 + // by the shell.
5238 + state = PreloadFlushed;
5239 }
5240 const props: StylesheetProps = Object.assign(
5241 ({
@@ -5185,17 +5245,17 @@ function preinitStyle(
5245 }: StylesheetProps),
5246 options,
5247 );
5188 - resource = {
5248 + const resource = {
5249 type: 'stylesheet',
5250 chunks: ([]: Array<Chunk | PrecomputedChunk>),
5251 state,
5252 props,
5253 };
5194 - resumableState.stylesMap.set(key, resource);
5195 - let precedenceSet = resumableState.precedences.get(precedence);
5196 - if (!precedenceSet) {
5197 - precedenceSet = new Set();
5198 - resumableState.precedences.set(precedence, precedenceSet);
5254 + resumableState.stylesMap[key] = null;
5255 + let stylesInPrecedence = renderState.precedences.get(precedence);
5256 + if (!stylesInPrecedence) {
5257 + stylesInPrecedence = new Map();
5258 + renderState.precedences.set(precedence, stylesInPrecedence);
5259 const emptyStyleResource = {
5260 type: 'style',
5261 chunks: ([]: Array<Chunk | PrecomputedChunk>),
@@ -5205,18 +5265,18 @@ function preinitStyle(
5265 hrefs: ([]: Array<string>),
5266 },
5267 };
5208 - precedenceSet.add(emptyStyleResource);
5268 + stylesInPrecedence.set('', emptyStyleResource);
5269 if (__DEV__) {
5210 - if (resumableState.stylePrecedences.has(precedence)) {
5270 + if (renderState.stylePrecedences.has(precedence)) {
5271 console.error(
5272 'React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.',
5273 precedence,
5274 );
5275 }
5276 }
5217 - resumableState.stylePrecedences.set(precedence, emptyStyleResource);
5277 + renderState.stylePrecedences.set(precedence, emptyStyleResource);
5278 }
5219 - precedenceSet.add(resource);
5279 + stylesInPrecedence.set(key, resource);
5280 flushResources(request);
5281 }
5282 return;
@@ -5237,17 +5297,17 @@ function preinitScript(src: string, options?: ?PreinitScriptOptions): void {
5297 return;
5298 }
5299 const resumableState = getResumableState(request);
5300 + const renderState = getRenderState(request);
5301 if (src) {
5302 const key = getResourceKey('script', src);
5242 - let resource = resumableState.scriptsMap.get(key);
5243 - if (!resource) {
5244 - resource = {
5303 + if (!resumableState.scriptsMap.hasOwnProperty(key)) {
5304 + const resource: ScriptResource = {
5305 type: 'script',
5306 chunks: [],
5307 state: NoState,
5308 props: null,
5309 };
5250 - resumableState.scriptsMap.set(key, resource);
5310 + resumableState.scriptsMap[key] = null;
5311 const props: ScriptProps = Object.assign(
5312 ({
5313 src,
@@ -5255,7 +5315,7 @@ function preinitScript(src: string, options?: ?PreinitScriptOptions): void {
5315 }: ScriptProps),
5316 options,
5317 );
5258 - resumableState.scripts.add(resource);
5318 + renderState.scripts.add(resource);
5319 pushScriptImpl(resource.chunks, props);
5320 flushResources(request);
5321 }
@@ -5280,17 +5340,17 @@ function preinitModuleScript(
5340 return;
5341 }
5342 const resumableState = getResumableState(request);
5343 + const renderState = getRenderState(request);
5344 if (src) {
5345 const key = getResourceKey('script', src);
5285 - let resource = resumableState.scriptsMap.get(key);
5286 - if (!resource) {
5287 - resource = {
5346 + if (!resumableState.scriptsMap.hasOwnProperty(key)) {
5347 + const resource: ScriptResource = {
5348 type: 'script',
5349 chunks: [],
5350 state: NoState,
5351 props: null,
5352 };
5293 - resumableState.scriptsMap.set(key, resource);
5353 + resumableState.scriptsMap[key] = null;
5354 const props = Object.assign(
5355 ({
5356 src,
@@ -5299,7 +5359,7 @@ function preinitModuleScript(
5359 }: ModuleProps),
5360 options,
5361 );
5302 - resumableState.scripts.add(resource);
5362 + renderState.scripts.add(resource);
5363 pushScriptImpl(resource.chunks, props);
5364 flushResources(request);
5365 }
@@ -5313,6 +5373,7 @@ function preinitModuleScript(
5373 // already exists and not assume it
5374 function preloadBootstrapScript(
5375 resumableState: ResumableState,
5376 + renderState: RenderState,
5377 src: string,
5378 nonce: ?string,
5379 integrity: ?string,
@@ -5320,7 +5381,7 @@ function preloadBootstrapScript(
5381 ): void {
5382 const key = getResourceKey('script', src);
5383 if (__DEV__) {
5323 - if (resumableState.preloadsMap.has(key)) {
5384 + if (resumableState.preloadsMap.hasOwnProperty(key)) {
5385 // This is coded as a React error because it should be impossible for a userspace preload to preempt this call
5386 // If a userspace preload can preempt it then this assumption is broken and we need to reconsider this strategy
5387 // rather than instruct the user to not preload their bootstrap scripts themselves
@@ -5345,8 +5406,9 @@ function preloadBootstrapScript(
5406 state: NoState,
5407 props,
5408 };
5348 - resumableState.preloadsMap.set(key, resource);
5349 - resumableState.bootstrapScripts.add(resource);
5409 + resumableState.preloadsMap[key] = props;
5410 + renderState.preloadsMap.set(key, resource);
5411 + renderState.bootstrapScripts.add(resource);
5412 pushLinkImpl(resource.chunks, props);
5413 }
5414
@@ -5356,6 +5418,7 @@ function preloadBootstrapScript(
5418 // already exists and not assume it
5419 function preloadBootstrapModule(
5420 resumableState: ResumableState,
5421 + renderState: RenderState,
5422 src: string,
5423 nonce: ?string,
5424 integrity: ?string,
@@ -5363,7 +5426,7 @@ function preloadBootstrapModule(
5426 ): void {
5427 const key = getResourceKey('script', src);
5428 if (__DEV__) {
5366 - if (resumableState.preloadsMap.has(key)) {
5429 + if (resumableState.preloadsMap.hasOwnProperty(key)) {
5430 // This is coded as a React error because it should be impossible for a userspace preload to preempt this call
5431 // If a userspace preload can preempt it then this assumption is broken and we need to reconsider this strategy
5432 // rather than instruct the user to not preload their bootstrap scripts themselves
@@ -5387,28 +5450,29 @@ function preloadBootstrapModule(
5450 state: NoState,
5451 props,
5452 };
5390 - resumableState.preloadsMap.set(key, resource);
5391 - resumableState.bootstrapScripts.add(resource);
5453 + resumableState.preloadsMap[key] = props;
5454 + renderState.preloadsMap.set(key, resource);
5455 + renderState.bootstrapScripts.add(resource);
5456 pushLinkImpl(resource.chunks, props);
5457 return;
5458 }
5459
5460 function internalPreinitScript(
5461 resumableState: ResumableState,
5462 + renderState: RenderState,
5463 src: string,
5464 chunks: Array<Chunk | PrecomputedChunk>,
5465 ): void {
5466 const key = getResourceKey('script', src);
5402 - let resource = resumableState.scriptsMap.get(key);
5403 - if (!resource) {
5404 - resource = {
5467 + if (!resumableState.scriptsMap.hasOwnProperty(key)) {
5468 + const resource: ScriptResource = {
5469 type: 'script',
5470 chunks,
5471 state: NoState,
5472 props: null,
5473 };
5410 - resumableState.scriptsMap.set(key, resource);
5411 - resumableState.scripts.add(resource);
5474 + resumableState.scriptsMap[key] = null;
5475 + renderState.scripts.add(resource);
5476 }
5477 return;
5478 }
packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js
+33 -2
@@ -39,11 +39,23 @@ export type RenderState = {
39 startInlineScript: PrecomputedChunk,
40 htmlChunks: null | Array<Chunk | PrecomputedChunk>,
41 headChunks: null | Array<Chunk | PrecomputedChunk>,
42 + externalRuntimeScript: null | any,
43 + bootstrapChunks: Array<Chunk | PrecomputedChunk>,
44 charsetChunks: Array<Chunk | PrecomputedChunk>,
45 preconnectChunks: Array<Chunk | PrecomputedChunk>,
46 importMapChunks: Array<Chunk | PrecomputedChunk>,
47 preloadChunks: Array<Chunk | PrecomputedChunk>,
48 hoistableChunks: Array<Chunk | PrecomputedChunk>,
49 + preconnects: Set<any>,
50 + fontPreloads: Set<any>,
51 + highImagePreloads: Set<any>,
52 + // usedImagePreloads: Set<any>,
53 + precedences: Map<string, Map<any, any>>,
54 + stylePrecedences: Map<string, any>,
55 + bootstrapScripts: Set<any>,
56 + scripts: Set<any>,
57 + bulkPreloads: Set<any>,
58 + preloadsMap: Map<string, any>,
59 boundaryResources: ?BoundaryResources,
60 stylesToHoist: boolean,
61 // This is an extra field for the legacy renderer
@@ -52,10 +64,17 @@ export type RenderState = {
64
65 export function createRenderState(
66 resumableState: ResumableState,
55 - nonce: string | void,
67 generateStaticMarkup: boolean,
68 ): RenderState {
58 - const renderState = createRenderStateImpl(resumableState, nonce);
69 + const renderState = createRenderStateImpl(
70 + resumableState,
71 + undefined,
72 + undefined,
73 + undefined,
74 + undefined,
75 + undefined,
76 + undefined,
77 + );
78 return {
79 // Keep this in sync with ReactFizzConfigDOM
80 placeholderPrefix: renderState.placeholderPrefix,
@@ -64,11 +83,23 @@ export function createRenderState(
83 startInlineScript: renderState.startInlineScript,
84 htmlChunks: renderState.htmlChunks,
85 headChunks: renderState.headChunks,
86 + externalRuntimeScript: renderState.externalRuntimeScript,
87 + bootstrapChunks: renderState.bootstrapChunks,
88 charsetChunks: renderState.charsetChunks,
89 preconnectChunks: renderState.preconnectChunks,
90 importMapChunks: renderState.importMapChunks,
91 preloadChunks: renderState.preloadChunks,
92 hoistableChunks: renderState.hoistableChunks,
93 + preconnects: renderState.preconnects,
94 + fontPreloads: renderState.fontPreloads,
95 + highImagePreloads: renderState.highImagePreloads,
96 + // usedImagePreloads: renderState.usedImagePreloads,
97 + precedences: renderState.precedences,
98 + stylePrecedences: renderState.stylePrecedences,
99 + bootstrapScripts: renderState.bootstrapScripts,
100 + scripts: renderState.scripts,
101 + bulkPreloads: renderState.bulkPreloads,
102 + preloadsMap: renderState.preloadsMap,
103 boundaryResources: renderState.boundaryResources,
104 stylesToHoist: renderState.stylesToHoist,
105
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+4 -4
@@ -6339,7 +6339,7 @@ describe('ReactDOMFizzServer', () => {
6339
6340 const resumed = ReactDOMFizzServer.resumeToPipeableStream(
6341 <App />,
6342 - prerendered.postponed,
6342 + JSON.parse(JSON.stringify(prerendered.postponed)),
6343 );
6344
6345 // Create a separate stream so it doesn't close the writable. I.e. simple concat.
@@ -6431,7 +6431,7 @@ describe('ReactDOMFizzServer', () => {
6431
6432 const resumed = ReactDOMFizzServer.resumeToPipeableStream(
6433 <App />,
6434 - prerendered.postponed,
6434 + JSON.parse(JSON.stringify(prerendered.postponed)),
6435 {
6436 onError(x) {
6437 ssrErrors.push(x.message);
@@ -6574,7 +6574,7 @@ describe('ReactDOMFizzServer', () => {
6574
6575 const resumed = ReactDOMFizzServer.resumeToPipeableStream(
6576 <App />,
6577 - prerendered.postponed,
6577 + JSON.parse(JSON.stringify(prerendered.postponed)),
6578 {
6579 onError(x) {
6580 ssrErrors.push(x.message);
@@ -6729,7 +6729,7 @@ describe('ReactDOMFizzServer', () => {
6729
6730 const resumed = ReactDOMFizzServer.resumeToPipeableStream(
6731 <App />,
6732 - prerendered.postponed,
6732 + JSON.parse(JSON.stringify(prerendered.postponed)),
6733 {
6734 onError(x) {
6735 ssrErrors.push(x.message);
packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
+145 -6
@@ -20,6 +20,7 @@ global.ReadableStream =
20 global.TextEncoder = require('util').TextEncoder;
21
22 let React;
23 +let ReactDOM;
24 let ReactDOMFizzServer;
25 let ReactDOMFizzStatic;
26 let Suspense;
@@ -29,6 +30,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
30 beforeEach(() => {
31 jest.resetModules();
32 React = require('react');
33 + ReactDOM = require('react-dom');
34 ReactDOMFizzServer = require('react-dom/server.browser');
35 if (__EXPERIMENTAL__) {
36 ReactDOMFizzStatic = require('react-dom/static.browser');
@@ -481,7 +483,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
483
484 const resumed = await ReactDOMFizzServer.resume(
485 <App />,
484 - prerendered.postponed,
486 + JSON.parse(JSON.stringify(prerendered.postponed)),
487 );
488
489 await readIntoContainer(prerendered.prelude);
@@ -523,7 +525,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
525
526 const resumed = await ReactDOMFizzServer.resume(
527 <App />,
526 - prerendered.postponed,
528 + JSON.parse(JSON.stringify(prerendered.postponed)),
529 );
530
531 await readIntoContainer(prerendered.prelude);
@@ -562,7 +564,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
564
565 const resumed = await ReactDOMFizzServer.resume(
566 <App />,
565 - prerendered.postponed,
567 + JSON.parse(JSON.stringify(prerendered.postponed)),
568 );
569
570 await readIntoContainer(prerendered.prelude);
@@ -610,7 +612,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
612
613 const resumed = await ReactDOMFizzServer.resume(
614 <App />,
613 - prerendered.postponed,
615 + JSON.parse(JSON.stringify(prerendered.postponed)),
616 );
617
618 await readIntoContainer(prerendered.prelude);
@@ -651,7 +653,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
653
654 const resumed = await ReactDOMFizzServer.resume(
655 <App />,
654 - prerendered.postponed,
656 + JSON.parse(JSON.stringify(prerendered.postponed)),
657 );
658
659 await readIntoContainer(prerendered.prelude);
@@ -692,7 +694,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
694
695 const content = await ReactDOMFizzServer.resume(
696 <App />,
695 - prerendered.postponed,
697 + JSON.parse(JSON.stringify(prerendered.postponed)),
698 );
699
700 const html = await readContent(concat(prerendered.prelude, content));
@@ -701,4 +703,141 @@ describe('ReactDOMFizzStaticBrowser', () => {
703 expect(Array.from(html.matchAll(htmlEndTags)).length).toBe(1);
704 expect(Array.from(html.matchAll(bodyEndTags)).length).toBe(1);
705 });
706 +
707 + // @gate enablePostpone
708 + it('can prerender various hoistables and deduped resources', async () => {
709 + let prerendering = true;
710 + function Postpone() {
711 + if (prerendering) {
712 + React.unstable_postpone();
713 + }
714 + return (
715 + <>
716 + <link rel="stylesheet" href="my-style2" precedence="low" />
717 + <link rel="stylesheet" href="my-style1" precedence="high" />
718 + <style precedence="high" href="my-style3">
719 + style
720 + </style>
721 + <img src="my-img" />
722 + </>
723 + );
724 + }
725 +
726 + function App() {
727 + ReactDOM.preconnect('example.com');
728 + ReactDOM.preload('my-font', {as: 'font', type: 'font/woff2'});
729 + ReactDOM.preload('my-style0', {as: 'style'});
730 + // This should transfer the props in to the style that loads later.
731 + ReactDOM.preload('my-style2', {
732 + as: 'style',
733 + crossOrigin: 'use-credentials',
734 + });
735 + return (
736 + <div>
737 + <Suspense fallback="Loading...">
738 + <link rel="stylesheet" href="my-style1" precedence="high" />
739 + <img src="my-img" />
740 + <Postpone />
741 + </Suspense>
742 + <title>Hello World</title>
743 + </div>
744 + );
745 + }
746 +
747 + let calledInit = false;
748 + jest.mock(
749 + 'init.js',
750 + () => {
751 + calledInit = true;
752 + },
753 + {virtual: true},
754 + );
755 +
756 + const prerendered = await ReactDOMFizzStatic.prerender(<App />, {
757 + bootstrapScripts: ['init.js'],
758 + });
759 + expect(prerendered.postponed).not.toBe(null);
760 +
761 + await readIntoContainer(prerendered.prelude);
762 +
763 + expect(getVisibleChildren(container)).toEqual([
764 + <link href="example.com" rel="preconnect" />,
765 + <link
766 + as="font"
767 + crossorigin=""
768 + href="my-font"
769 + rel="preload"
770 + type="font/woff2"
771 + />,
772 + <link as="image" href="my-img" rel="preload" />,
773 + <link data-precedence="high" href="my-style1" rel="stylesheet" />,
774 + <link as="script" fetchpriority="low" href="init.js" rel="preload" />,
775 + <link as="style" href="my-style0" rel="preload" />,
776 + <link
777 + as="style"
778 + crossorigin="use-credentials"
779 + href="my-style2"
780 + rel="preload"
781 + />,
782 + <title>Hello World</title>,
783 + <div>Loading...</div>,
784 + ]);
785 +
786 + prerendering = false;
787 + const content = await ReactDOMFizzServer.resume(
788 + <App />,
789 + JSON.parse(JSON.stringify(prerendered.postponed)),
790 + );
791 +
792 + await readIntoContainer(content);
793 +
794 + expect(calledInit).toBe(true);
795 +
796 + // Dispatch load event to injected stylesheet
797 + const link = document.querySelector(
798 + 'link[rel="stylesheet"][href="my-style2"]',
799 + );
800 + const event = document.createEvent('Events');
801 + event.initEvent('load', true, true);
802 + link.dispatchEvent(event);
803 +
804 + // Wait for the instruction microtasks to flush.
805 + await 0;
806 + await 0;
807 +
808 + expect(getVisibleChildren(container)).toEqual([
809 + <link href="example.com" rel="preconnect" />,
810 + <link
811 + as="font"
812 + crossorigin=""
813 + href="my-font"
814 + rel="preload"
815 + type="font/woff2"
816 + />,
817 + <link as="image" href="my-img" rel="preload" />,
818 + <link data-precedence="high" href="my-style1" rel="stylesheet" />,
819 + <style data-href="my-style3" data-precedence="high">
820 + style
821 + </style>,
822 + <link
823 + crossorigin="use-credentials"
824 + data-precedence="low"
825 + href="my-style2"
826 + rel="stylesheet"
827 + />,
828 + <link as="script" fetchpriority="low" href="init.js" rel="preload" />,
829 + <link as="style" href="my-style0" rel="preload" />,
830 + <link
831 + as="style"
832 + crossorigin="use-credentials"
833 + href="my-style2"
834 + rel="preload"
835 + />,
836 + <title>Hello World</title>,
837 + <div>
838 + <img src="my-img" />
839 + <img src="my-img" />
840 + </div>,
841 + ]);
842 + });
843 });
packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
+6 -6
@@ -25,6 +25,7 @@ import {
25 import {
26 createResumableState,
27 createRenderState,
28 + resumeRenderState,
29 createRootFormatContext,
30 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
31
@@ -96,10 +97,6 @@ function renderToReadableStream(
97 }
98 const resumableState = createResumableState(
99 options ? options.identifierPrefix : undefined,
99 - options ? options.nonce : undefined,
100 - options ? options.bootstrapScriptContent : undefined,
101 - options ? options.bootstrapScripts : undefined,
102 - options ? options.bootstrapModules : undefined,
100 options ? options.unstable_externalRuntimeSrc : undefined,
101 );
102 const request = createRequest(
@@ -108,6 +105,10 @@ function renderToReadableStream(
105 createRenderState(
106 resumableState,
107 options ? options.nonce : undefined,
108 + options ? options.bootstrapScriptContent : undefined,
109 + options ? options.bootstrapScripts : undefined,
110 + options ? options.bootstrapModules : undefined,
111 + options ? options.unstable_externalRuntimeSrc : undefined,
112 options ? options.importMap : undefined,
113 ),
114 createRootFormatContext(options ? options.namespaceURI : undefined),
@@ -177,10 +178,9 @@ function resume(
178 const request = resumeRequest(
179 children,
180 postponedState,
180 - createRenderState(
181 + resumeRenderState(
182 postponedState.resumableState,
183 options ? options.nonce : undefined,
183 - undefined, // importMap
184 ),
185 options ? options.onError : undefined,
186 onAllReady,
packages/react-dom/src/server/ReactDOMFizzServerBun.js
+4 -4
@@ -87,10 +87,6 @@ function renderToReadableStream(
87 }
88 const resumableState = createResumableState(
89 options ? options.identifierPrefix : undefined,
90 - options ? options.nonce : undefined,
91 - options ? options.bootstrapScriptContent : undefined,
92 - options ? options.bootstrapScripts : undefined,
93 - options ? options.bootstrapModules : undefined,
90 options ? options.unstable_externalRuntimeSrc : undefined,
91 );
92 const request = createRequest(
@@ -99,6 +95,10 @@ function renderToReadableStream(
95 createRenderState(
96 resumableState,
97 options ? options.nonce : undefined,
98 + options ? options.bootstrapScriptContent : undefined,
99 + options ? options.bootstrapScripts : undefined,
100 + options ? options.bootstrapModules : undefined,
101 + options ? options.unstable_externalRuntimeSrc : undefined,
102 options ? options.importMap : undefined,
103 ),
104 createRootFormatContext(options ? options.namespaceURI : undefined),
packages/react-dom/src/server/ReactDOMFizzServerEdge.js
+6 -6
@@ -25,6 +25,7 @@ import {
25 import {
26 createResumableState,
27 createRenderState,
28 + resumeRenderState,
29 createRootFormatContext,
30 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
31
@@ -96,10 +97,6 @@ function renderToReadableStream(
97 }
98 const resumableState = createResumableState(
99 options ? options.identifierPrefix : undefined,
99 - options ? options.nonce : undefined,
100 - options ? options.bootstrapScriptContent : undefined,
101 - options ? options.bootstrapScripts : undefined,
102 - options ? options.bootstrapModules : undefined,
100 options ? options.unstable_externalRuntimeSrc : undefined,
101 );
102 const request = createRequest(
@@ -108,6 +105,10 @@ function renderToReadableStream(
105 createRenderState(
106 resumableState,
107 options ? options.nonce : undefined,
108 + options ? options.bootstrapScriptContent : undefined,
109 + options ? options.bootstrapScripts : undefined,
110 + options ? options.bootstrapModules : undefined,
111 + options ? options.unstable_externalRuntimeSrc : undefined,
112 options ? options.importMap : undefined,
113 ),
114 createRootFormatContext(options ? options.namespaceURI : undefined),
@@ -177,10 +178,9 @@ function resume(
178 const request = resumeRequest(
179 children,
180 postponedState,
180 - createRenderState(
181 + resumeRenderState(
182 postponedState.resumableState,
183 options ? options.nonce : undefined,
183 - undefined, // importMap
184 ),
185 options ? options.onError : undefined,
186 onAllReady,
packages/react-dom/src/server/ReactDOMFizzServerNode.js
+6 -6
@@ -27,6 +27,7 @@ import {
27 import {
28 createResumableState,
29 createRenderState,
30 + resumeRenderState,
31 createRootFormatContext,
32 } from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
33
@@ -76,10 +77,6 @@ type PipeableStream = {
77 function createRequestImpl(children: ReactNodeList, options: void | Options) {
78 const resumableState = createResumableState(
79 options ? options.identifierPrefix : undefined,
79 - options ? options.nonce : undefined,
80 - options ? options.bootstrapScriptContent : undefined,
81 - options ? options.bootstrapScripts : undefined,
82 - options ? options.bootstrapModules : undefined,
80 options ? options.unstable_externalRuntimeSrc : undefined,
81 );
82 return createRequest(
@@ -88,6 +85,10 @@ function createRequestImpl(children: ReactNodeList, options: void | Options) {
85 createRenderState(
86 resumableState,
87 options ? options.nonce : undefined,
88 + options ? options.bootstrapScriptContent : undefined,
89 + options ? options.bootstrapScripts : undefined,
90 + options ? options.bootstrapModules : undefined,
91 + options ? options.unstable_externalRuntimeSrc : undefined,
92 options ? options.importMap : undefined,
93 ),
94 createRootFormatContext(options ? options.namespaceURI : undefined),
@@ -146,10 +147,9 @@ function resumeRequestImpl(
147 return resumeRequest(
148 children,
149 postponedState,
149 - createRenderState(
150 + resumeRenderState(
151 postponedState.resumableState,
152 options ? options.nonce : undefined,
152 - undefined, // importMap
153 ),
154 options ? options.onError : undefined,
155 options ? options.onAllReady : undefined,
packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js
+5 -5
@@ -74,10 +74,6 @@ function prerender(
74 }
75 const resources = createResumableState(
76 options ? options.identifierPrefix : undefined,
77 - undefined, // nonce is not compatible with prerendered bootstrap scripts
78 - options ? options.bootstrapScriptContent : undefined,
79 - options ? options.bootstrapScripts : undefined,
80 - options ? options.bootstrapModules : undefined,
77 options ? options.unstable_externalRuntimeSrc : undefined,
78 );
79 const request = createPrerenderRequest(
@@ -85,7 +81,11 @@ function prerender(
81 resources,
82 createRenderState(
83 resources,
88 - undefined, // nonce
84 + undefined, // nonce is not compatible with prerendered bootstrap scripts
85 + options ? options.bootstrapScriptContent : undefined,
86 + options ? options.bootstrapScripts : undefined,
87 + options ? options.bootstrapModules : undefined,
88 + options ? options.unstable_externalRuntimeSrc : undefined,
89 options ? options.importMap : undefined,
90 ),
91 createRootFormatContext(options ? options.namespaceURI : undefined),
packages/react-dom/src/server/ReactDOMFizzStaticEdge.js
+5 -5
@@ -74,10 +74,6 @@ function prerender(
74 }
75 const resources = createResumableState(
76 options ? options.identifierPrefix : undefined,
77 - undefined, // nonce is not compatible with prerendered bootstrap scripts
78 - options ? options.bootstrapScriptContent : undefined,
79 - options ? options.bootstrapScripts : undefined,
80 - options ? options.bootstrapModules : undefined,
77 options ? options.unstable_externalRuntimeSrc : undefined,
78 );
79 const request = createPrerenderRequest(
@@ -85,7 +81,11 @@ function prerender(
81 resources,
82 createRenderState(
83 resources,
88 - undefined, // nonce
84 + undefined, // nonce is not compatible with prerendered bootstrap scripts
85 + options ? options.bootstrapScriptContent : undefined,
86 + options ? options.bootstrapScripts : undefined,
87 + options ? options.bootstrapModules : undefined,
88 + options ? options.unstable_externalRuntimeSrc : undefined,
89 options ? options.importMap : undefined,
90 ),
91 createRootFormatContext(options ? options.namespaceURI : undefined),
packages/react-dom/src/server/ReactDOMFizzStaticNode.js
+5 -5
@@ -88,10 +88,6 @@ function prerenderToNodeStream(
88 }
89 const resumableState = createResumableState(
90 options ? options.identifierPrefix : undefined,
91 - undefined, // nonce is not compatible with prerendered bootstrap scripts
92 - options ? options.bootstrapScriptContent : undefined,
93 - options ? options.bootstrapScripts : undefined,
94 - options ? options.bootstrapModules : undefined,
91 options ? options.unstable_externalRuntimeSrc : undefined,
92 );
93 const request = createPrerenderRequest(
@@ -99,7 +95,11 @@ function prerenderToNodeStream(
95 resumableState,
96 createRenderState(
97 resumableState,
102 - undefined, // nonce
98 + undefined, // nonce is not compatible with prerendered bootstrap scripts
99 + options ? options.bootstrapScriptContent : undefined,
100 + options ? options.bootstrapScripts : undefined,
101 + options ? options.bootstrapModules : undefined,
102 + options ? options.unstable_externalRuntimeSrc : undefined,
103 options ? options.importMap : undefined,
104 ),
105 createRootFormatContext(options ? options.namespaceURI : undefined),
packages/react-dom/src/server/ReactDOMLegacyServerImpl.js
+1 -5
@@ -63,15 +63,11 @@ function renderToStringImpl(
63 const resumableState = createResumableState(
64 options ? options.identifierPrefix : undefined,
65 undefined,
66 - undefined,
67 - undefined,
68 - undefined,
69 - undefined,
66 );
67 const request = createRequest(
68 children,
69 resumableState,
74 - createRenderState(resumableState, undefined, generateStaticMarkup),
70 + createRenderState(resumableState, generateStaticMarkup),
71 createRootFormatContext(),
72 Infinity,
73 onError,
packages/react-dom/src/server/ReactDOMLegacyServerNodeStream.js
+1 -5
@@ -74,15 +74,11 @@ function renderToNodeStreamImpl(
74 const resumableState = createResumableState(
75 options ? options.identifierPrefix : undefined,
76 undefined,
77 - undefined,
78 - undefined,
79 - undefined,
80 - undefined,
77 );
78 const request = createRequest(
79 children,
80 resumableState,
85 - createRenderState(resumableState, undefined, false),
81 + createRenderState(resumableState, false),
82 createRootFormatContext(),
83 Infinity,
84 onError,
packages/react-server-dom-fb/src/ReactDOMServerFB.js
+8 -5
@@ -52,16 +52,19 @@ function renderToStream(children: ReactNodeList, options: Options): Stream {
52 };
53 const resumableState = createResumableState(
54 options ? options.identifierPrefix : undefined,
55 - undefined,
56 - options ? options.bootstrapScriptContent : undefined,
57 - options ? options.bootstrapScripts : undefined,
58 - options ? options.bootstrapModules : undefined,
55 options ? options.unstable_externalRuntimeSrc : undefined,
56 );
57 const request = createRequest(
58 children,
59 resumableState,
64 - createRenderState(resumableState, undefined),
60 + createRenderState(
61 + resumableState,
62 + undefined,
63 + options ? options.bootstrapScriptContent : undefined,
64 + options ? options.bootstrapScripts : undefined,
65 + options ? options.bootstrapModules : undefined,
66 + options ? options.unstable_externalRuntimeSrc : undefined,
67 + ),
68 createRootFormatContext(undefined),
69 options ? options.progressiveChunkSize : undefined,
70 options.onError,
packages/react-server/src/ReactFizzServer.js
+8 -2
@@ -3962,13 +3962,15 @@ function flushCompletedQueues(
3962 destination,
3963 request.resumableState,
3964 request.renderState,
3965 - request.allPendingTasks === 0,
3965 + request.allPendingTasks === 0 &&
3966 + (request.trackedPostpones === null ||
3967 + request.trackedPostpones.workingMap.size === 0),
3968 );
3969 }
3970
3971 flushSegment(request, destination, completedRootSegment);
3972 request.completedRootSegment = null;
3971 - writeCompletedRoot(destination, request.resumableState);
3973 + writeCompletedRoot(destination, request.renderState);
3974 } else {
3975 // We haven't flushed the root yet so we don't need to check any other branches further down
3976 return;
@@ -4166,6 +4168,10 @@ export function getResumableState(request: Request): ResumableState {
4168 return request.resumableState;
4169 }
4170
4171 +export function getRenderState(request: Request): RenderState {
4172 + return request.renderState;
4173 +}
4174 +
4175 function addToReplayParent(
4176 node: ResumableNode,
4177 parentKeyPath: Root | KeyNode,