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