@samitouri / QOS-React / commits / 49eba01930

[Fizz][Float] Refactor Resources (#27400)

Refactors Resources to have a more compact and memory efficient struture. Resources generally are just an Array of chunks. A resource is flushed when it's chunks is length zero. A resource does not have any other state. Stylesheets and Style tags are different and have been modeled as a unit as a StyleQueue. This object stores the style rules to flush as part of style tags using precedence as well as all the stylesheets associated with the precedence. Stylesheets still need to track state because it affects how we issue boundary completion instructions. Additionally stylesheets encode chunks lazily because we may never write them as html if they are discovered late. The preload props transfer is now maximally compact (only stores the props we would ever actually adopt) and only stores props for stylesheets and scripts because other preloads have no resource counterpart to adopt props into. The ResumableState maps that track which keys have been observed are being overloaded. Previously if a key was found it meant that a resource already exists (either in this render or in a prior prerender). Now we discriminate between null and object values. If map value is null we can assume the resource exists but if it is an object that represents a prior preload for that resource and the resource must still be constructed.

Josh Story committed Sep 26, 2023 at 09:59 UTC 49eba01930e9e1f331b34967fca65d5a0ba62846
11 files changed +1710 -689
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+823 -666
@@ -148,18 +148,22 @@ export type RenderState = {
148 hoistableChunks: Array<Chunk | PrecomputedChunk>,
149
150 // Flushing queues for Resource dependencies
151 - preconnects: Set<PreconnectResource>,
152 - fontPreloads: Set<PreloadResource>,
153 - highImagePreloads: Set<PreloadResource>,
151 + preconnects: Set<Resource>,
152 + fontPreloads: Set<Resource>,
153 + highImagePreloads: Set<Resource>,
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>,
155 + styles: Map<string, StyleQueue>,
156 + bootstrapScripts: Set<Resource>,
157 + scripts: Set<Resource>,
158 + bulkPreloads: Set<Resource>,
159
160 // Temporarily keeps track of key to preload resources before shell flushes.
162 - preloadsMap: Map<string, PreloadResource>,
161 + preloads: {
162 + images: Map<string, Resource>,
163 + stylesheets: Map<string, Resource>,
164 + scripts: Map<string, Resource>,
165 + moduleScripts: Map<string, Resource>,
166 + },
167
168 // Module-global-like reference for current boundary resources
169 boundaryResources: ?BoundaryResources,
@@ -174,6 +178,30 @@ export type RenderState = {
178 ...
179 };
180
181 +type Exists = null;
182 +type Preloaded = [];
183 +// Credentials here are things that affect whether a browser will make a request
184 +// as well as things that affect which connection the browser will use for that request.
185 +// We want these to be aligned across preloads and resources because otherwise the preload
186 +// will be wasted.
187 +// We investigated whether referrerPolicy should be included here but from experimentation
188 +// it seems that browsers do not treat this as part of the http cache key and does not affect
189 +// which connection is used.
190 +type PreloadedWithCredentials = [
191 + /* crossOrigin */ ?string,
192 + /* integrity */ ?string,
193 +];
194 +
195 +const EXISTS: Exists = null;
196 +// This constant is to mark preloads that have no unique credentials
197 +// to convey. It should never be checked by identity and we should not
198 +// assume Preload values in ResumableState equal this value because they
199 +// will have come from some parsed input.
200 +const PRELOAD_NO_CREDS: Preloaded = [];
201 +if (__DEV__) {
202 + Object.freeze(PRELOAD_NO_CREDS);
203 +}
204 +
205 // Per response, global state that is not contextual to the rendering subtree.
206 // This is resumable and therefore should be serializable.
207 export type ResumableState = {
@@ -189,10 +217,34 @@ export type ResumableState = {
217 hasHtml: boolean,
218
219 // Resources - Request local cache
192 - preloadsMap: {[key: string]: PreloadProps},
193 - preconnectsMap: {[key: string]: null},
194 - stylesMap: {[key: string]: null},
195 - scriptsMap: {[key: string]: null},
220 + unknownResources: {
221 + [asType: string]: {
222 + [href: string]: Preloaded,
223 + },
224 + },
225 + dnsResources: {[key: string]: Exists},
226 + connectResources: {
227 + default: {[key: string]: Exists},
228 + anonymous: {[key: string]: Exists},
229 + credentials: {[key: string]: Exists},
230 + },
231 + imageResources: {
232 + [key: string]: Preloaded,
233 + },
234 + styleResources: {
235 + [key: string]: Exists | Preloaded | PreloadedWithCredentials,
236 + },
237 + scriptResources: {
238 + [key: string]: Exists | Preloaded | PreloadedWithCredentials,
239 + },
240 + moduleUnknownResources: {
241 + [asType: string]: {
242 + [href: string]: Preloaded,
243 + },
244 + },
245 + moduleScriptResources: {
246 + [key: string]: Exists | Preloaded | PreloadedWithCredentials,
247 + },
248 };
249
250 const dataElementQuotedEnd = stringToPrecomputedChunk('"></template>');
@@ -342,13 +394,17 @@ export function createRenderState(
394 fontPreloads: new Set(),
395 highImagePreloads: new Set(),
396 // usedImagePreloads: new Set(),
345 - precedences: new Map(),
346 - stylePrecedences: new Map(),
397 + styles: new Map(),
398 bootstrapScripts: new Set(),
399 scripts: new Set(),
400 bulkPreloads: new Set(),
401
351 - preloadsMap: new Map(),
402 + preloads: {
403 + images: new Map(),
404 + stylesheets: new Map(),
405 + scripts: new Map(),
406 + moduleScripts: new Map(),
407 + },
408
409 nonce,
410 // like a module global for currently rendering boundary
@@ -359,25 +415,30 @@ export function createRenderState(
415 if (bootstrapScripts !== undefined) {
416 for (let i = 0; i < bootstrapScripts.length; i++) {
417 const scriptConfig = bootstrapScripts[i];
362 - const src =
363 - typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src;
364 - const integrity =
365 - typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity;
366 - const crossOrigin =
367 - typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null
368 - ? undefined
369 - : scriptConfig.crossOrigin === 'use-credentials'
370 - ? 'use-credentials'
371 - : '';
372 -
373 - preloadBootstrapScript(
374 - resumableState,
375 - renderState,
376 - src,
418 + let src, crossOrigin, integrity;
419 + const props: PreloadAsProps = ({
420 + rel: 'preload',
421 + as: 'script',
422 + fetchPriority: 'low',
423 nonce,
378 - integrity,
379 - crossOrigin,
380 - );
424 + }: any);
425 + if (typeof scriptConfig === 'string') {
426 + props.href = src = scriptConfig;
427 + } else {
428 + props.href = src = scriptConfig.src;
429 + props.integrity = integrity =
430 + typeof scriptConfig.integrity === 'string'
431 + ? scriptConfig.integrity
432 + : undefined;
433 + props.crossOrigin = crossOrigin =
434 + typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null
435 + ? undefined
436 + : scriptConfig.crossOrigin === 'use-credentials'
437 + ? 'use-credentials'
438 + : '';
439 + }
440 +
441 + preloadBootstrapScriptOrModule(resumableState, renderState, src, props);
442
443 bootstrapChunks.push(
444 startScriptSrc,
@@ -389,7 +450,7 @@ export function createRenderState(
450 stringToChunk(escapeTextForBrowser(nonce)),
451 );
452 }
392 - if (integrity) {
453 + if (typeof integrity === 'string') {
454 bootstrapChunks.push(
455 scriptIntegirty,
456 stringToChunk(escapeTextForBrowser(integrity)),
@@ -407,25 +468,29 @@ export function createRenderState(
468 if (bootstrapModules !== undefined) {
469 for (let i = 0; i < bootstrapModules.length; i++) {
470 const scriptConfig = bootstrapModules[i];
410 - const src =
411 - typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src;
412 - const integrity =
413 - typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity;
414 - const crossOrigin =
415 - typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null
416 - ? undefined
417 - : scriptConfig.crossOrigin === 'use-credentials'
418 - ? 'use-credentials'
419 - : '';
420 -
421 - preloadBootstrapModule(
422 - resumableState,
423 - renderState,
424 - src,
471 + let src, crossOrigin, integrity;
472 + const props: PreloadModuleProps = ({
473 + rel: 'modulepreload',
474 + fetchPriority: 'low',
475 nonce,
426 - integrity,
427 - crossOrigin,
428 - );
476 + }: any);
477 + if (typeof scriptConfig === 'string') {
478 + props.href = src = scriptConfig;
479 + } else {
480 + props.href = src = scriptConfig.src;
481 + props.integrity = integrity =
482 + typeof scriptConfig.integrity === 'string'
483 + ? scriptConfig.integrity
484 + : undefined;
485 + props.crossOrigin = crossOrigin =
486 + typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null
487 + ? undefined
488 + : scriptConfig.crossOrigin === 'use-credentials'
489 + ? 'use-credentials'
490 + : '';
491 + }
492 +
493 + preloadBootstrapScriptOrModule(resumableState, renderState, src, props);
494
495 bootstrapChunks.push(
496 startModuleSrc,
@@ -438,7 +503,7 @@ export function createRenderState(
503 stringToChunk(escapeTextForBrowser(nonce)),
504 );
505 }
441 - if (integrity) {
506 + if (typeof integrity === 'string') {
507 bootstrapChunks.push(
508 scriptIntegirty,
509 stringToChunk(escapeTextForBrowser(integrity)),
@@ -496,10 +561,18 @@ export function createResumableState(
561 // @TODO add bootstrap script to implicit preloads
562
563 // persistent
499 - preloadsMap: {},
500 - preconnectsMap: {},
501 - stylesMap: {},
502 - scriptsMap: {},
564 + unknownResources: {},
565 + dnsResources: {},
566 + connectResources: {
567 + default: {},
568 + anonymous: {},
569 + credentials: {},
570 + },
571 + imageResources: {},
572 + styleResources: {},
573 + scriptResources: {},
574 + moduleUnknownResources: {},
575 + moduleScriptResources: {},
576 };
577 }
578
@@ -2102,7 +2175,7 @@ function pushLink(
2175
2176 if (props.rel === 'stylesheet') {
2177 // This <link> may hoistable as a Stylesheet Resource, otherwise it will emit in place
2105 - const key = getResourceKey('style', href);
2178 + const key = getResourceKey(href);
2179 if (
2180 typeof precedence !== 'string' ||
2181 props.disabled != null ||
@@ -2136,60 +2209,62 @@ function pushLink(
2209 return pushLinkImpl(target, props);
2210 } else {
2211 // This stylesheet refers to a Resource and we create a new one if necessary
2139 - let stylesInPrecedence = renderState.precedences.get(precedence);
2140 - if (!resumableState.stylesMap.hasOwnProperty(key)) {
2141 - const resourceProps = stylesheetPropsFromRawProps(props);
2142 - let state = NoState;
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 - }
2212 + let styleQueue = renderState.styles.get(precedence);
2213 + const hasKey = resumableState.styleResources.hasOwnProperty(key);
2214 + const resourceState = hasKey
2215 + ? resumableState.styleResources[key]
2216 + : undefined;
2217 + if (resourceState !== EXISTS) {
2218 + // We are going to create this resource now so it is marked as Exists
2219 + resumableState.styleResources[key] = EXISTS;
2220 +
2221 + // If this is the first time we've encountered this precedence we need
2222 + // to create a StyleQueue
2223 + if (!styleQueue) {
2224 + styleQueue = {
2225 + precedence: stringToChunk(escapeTextForBrowser(precedence)),
2226 + rules: ([]: Array<Chunk | PrecomputedChunk>),
2227 + hrefs: ([]: Array<Chunk | PrecomputedChunk>),
2228 + sheets: (new Map(): Map<string, StylesheetResource>),
2229 + };
2230 + renderState.styles.set(precedence, styleQueue);
2231 }
2160 - const resource = {
2161 - type: 'stylesheet',
2162 - chunks: ([]: Array<Chunk | PrecomputedChunk>),
2163 - state,
2164 - props: resourceProps,
2232 +
2233 + const resource: StylesheetResource = {
2234 + state: PENDING,
2235 + props: stylesheetPropsFromRawProps(props),
2236 };
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>),
2173 - state: NoState,
2174 - props: {
2175 - precedence,
2176 - hrefs: ([]: Array<string>),
2177 - },
2178 - };
2179 - stylesInPrecedence.set('', emptyStyleResource);
2180 - if (__DEV__) {
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 - }
2237 +
2238 + if (resourceState) {
2239 + // When resourceState is truty it is a Preload state. We cast it for clarity
2240 + const preloadState: Preloaded | PreloadedWithCredentials =
2241 + resourceState;
2242 + if (preloadState.length === 2) {
2243 + adoptPreloadCredentials(resource.props, preloadState);
2244 + }
2245 +
2246 + const preloadResource = renderState.preloads.stylesheets.get(key);
2247 + if (preloadResource && preloadResource.length > 0) {
2248 + // The Preload for this resource was created in this render pass and has not flushed yet so
2249 + // we need to clear it to avoid it flushing.
2250 + preloadResource.length = 0;
2251 + } else {
2252 + // Either the preload resource from this render already flushed in this render pass
2253 + // or the preload flushed in a prior pass (prerender). In either case we need to mark
2254 + // this resource as already having been preloaded.
2255 + resource.state = PRELOADED;
2256 }
2188 - renderState.stylePrecedences.set(precedence, emptyStyleResource);
2257 + } else {
2258 + // We don't need to check whether a preloadResource exists in the renderState
2259 + // because if it did exist then the resourceState would also exist and we would
2260 + // have hit the primary if condition above.
2261 }
2190 - stylesInPrecedence.set(key, resource);
2262 +
2263 + // We add the newly created resource to our StyleQueue and if necessary
2264 + // track the resource with the currently rendering boundary
2265 + styleQueue.sheets.set(key, resource);
2266 if (renderState.boundaryResources) {
2192 - renderState.boundaryResources.add(resource);
2267 + renderState.boundaryResources.stylesheets.add(resource);
2268 }
2269 } else {
2270 // We need to track whether this boundary should wait on this resource or not.
@@ -2197,11 +2272,11 @@ function pushLink(
2272 // it. However, it's possible when you resume that the style has already been emitted
2273 // and then it wouldn't be recreated in the RenderState and there's no need to track
2274 // it again since we should've hoisted it to the shell already.
2200 - if (stylesInPrecedence) {
2201 - const resource = stylesInPrecedence.get(key);
2275 + if (styleQueue) {
2276 + const resource = styleQueue.sheets.get(key);
2277 if (resource) {
2278 if (renderState.boundaryResources) {
2204 - renderState.boundaryResources.add(resource);
2279 + renderState.boundaryResources.stylesheets.add(resource);
2280 }
2281 }
2282 }
@@ -2334,45 +2409,49 @@ function pushStyle(
2409 }
2410 }
2411
2337 - const key = getResourceKey('style', href);
2338 - let resource = renderState.stylePrecedences.get(precedence);
2339 - if (!resumableState.stylesMap.hasOwnProperty(key)) {
2340 - if (!resource) {
2341 - resource = {
2342 - type: 'style',
2343 - chunks: [],
2344 - state: NoState,
2345 - props: {
2346 - precedence,
2347 - hrefs: [href],
2348 - },
2349 - };
2350 - renderState.stylePrecedences.set(precedence, resource);
2351 - const stylesInPrecedence: Map<string, StyleResource> = new Map();
2352 - stylesInPrecedence.set('', resource);
2353 - if (__DEV__) {
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 - }
2412 + const key = getResourceKey(href);
2413 + let styleQueue = renderState.styles.get(precedence);
2414 + const hasKey = resumableState.styleResources.hasOwnProperty(key);
2415 + const resourceState = hasKey
2416 + ? resumableState.styleResources[key]
2417 + : undefined;
2418 + if (resourceState !== EXISTS) {
2419 + // We are going to create this resource now so it is marked as Exists
2420 + resumableState.styleResources[key] = EXISTS;
2421 +
2422 + if (__DEV__) {
2423 + if (resourceState) {
2424 + console.error(
2425 + 'React encountered a hoistable style tag for the same href as a preload: "%s". When using a style tag to inline styles you should not also preload it as a stylsheet.',
2426 + href,
2427 + );
2428 }
2361 - renderState.precedences.set(precedence, stylesInPrecedence);
2429 + }
2430 +
2431 + if (!styleQueue) {
2432 + // This is the first time we've encountered this precedence we need
2433 + // to create a StyleQueue.
2434 + styleQueue = {
2435 + precedence: stringToChunk(escapeTextForBrowser(precedence)),
2436 + rules: ([]: Array<Chunk | PrecomputedChunk>),
2437 + hrefs: [stringToChunk(escapeTextForBrowser(href))],
2438 + sheets: (new Map(): Map<string, StylesheetResource>),
2439 + };
2440 + renderState.styles.set(precedence, styleQueue);
2441 } else {
2363 - resource.props.hrefs.push(href);
2442 + // We have seen this precedence before and need to track this href
2443 + styleQueue.hrefs.push(stringToChunk(escapeTextForBrowser(href)));
2444 }
2365 - resumableState.stylesMap[key] = null;
2366 - pushStyleContents(resource.chunks, props);
2445 + pushStyleContents(styleQueue.rules, props);
2446 }
2368 - if (resource) {
2447 + if (styleQueue) {
2448 // We need to track whether this boundary should wait on this resource or not.
2449 // Typically this resource should always exist since we either had it or just created
2450 // it. However, it's possible when you resume that the style has already been emitted
2451 // and then it wouldn't be recreated in the RenderState and there's no need to track
2452 // it again since we should've hoisted it to the shell already.
2453 if (renderState.boundaryResources) {
2375 - renderState.boundaryResources.add(resource);
2454 + renderState.boundaryResources.styles.add(styleQueue);
2455 }
2456 }
2457
@@ -2475,23 +2554,6 @@ function pushStyleContents(
2554 return;
2555 }
2556
2478 -function getImagePreloadKey(
2479 - href: string,
2480 - imageSrcSet: ?string,
2481 - imageSizes: ?string,
2482 -) {
2483 - let uniquePart = '';
2484 - if (typeof imageSrcSet === 'string' && imageSrcSet !== '') {
2485 - uniquePart += '[' + imageSrcSet + ']';
2486 - if (typeof imageSizes === 'string') {
2487 - uniquePart += '[' + imageSizes + ']';
2488 - }
2489 - } else {
2490 - uniquePart += '[][]' + href;
2491 - }
2492 - return getResourceKey('image', uniquePart);
2493 -}
2494 -
2557 function pushImg(
2558 target: Array<Chunk | PrecomputedChunk>,
2559 props: Object,
@@ -2502,7 +2564,9 @@ function pushImg(
2564 const {src, srcSet} = props;
2565 if (
2566 props.loading !== 'lazy' &&
2505 - (typeof src === 'string' || typeof srcSet === 'string') &&
2567 + (src || srcSet) &&
2568 + (typeof src === 'string' || src == null) &&
2569 + (typeof srcSet === 'string' || srcSet == null) &&
2570 props.fetchPriority !== 'low' &&
2571 pictureTagInScope === false &&
2572 // We exclude data URIs in src and srcSet since these should not be preloaded
@@ -2525,39 +2589,50 @@ function pushImg(
2589 ) {
2590 // We have a suspensey image and ought to preload it to optimize the loading of display blocking
2591 // resumableState.
2528 - const {sizes} = props;
2529 - const key = getImagePreloadKey(src, srcSet, sizes);
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,
2552 - props: preloadProps,
2553 - };
2554 - resumableState.preloadsMap[key] = preloadProps;
2555 - renderState.preloadsMap.set(key, resource);
2556 - pushLinkImpl(resource.chunks, preloadProps);
2557 - } else {
2558 - resource = renderState.preloadsMap.get(key);
2559 - }
2592 + const sizes = typeof props.sizes === 'string' ? props.sizes : undefined;
2593 + const key = getImageResourceKey(src, srcSet, sizes);
2594 +
2595 + const promotablePreloads = renderState.preloads.images;
2596 +
2597 + let resource = promotablePreloads.get(key);
2598 if (resource) {
2599 + // We consider whether this preload can be promoted to higher priority flushing queue.
2600 + // The only time a resource will exist here is if it was created during this render
2601 + // and was not already in the high priority queue.
2602 + if (
2603 + props.fetchPriority === 'high' ||
2604 + renderState.highImagePreloads.size < 10
2605 + ) {
2606 + // Delete the resource from the map since we are promoting it and don't want to
2607 + // reenter this branch in a second pass for duplicate img hrefs.
2608 + promotablePreloads.delete(key);
2609 +
2610 + // $FlowFixMe - Flow should understand that this is a Resource if the condition was true
2611 + renderState.highImagePreloads.add(resource);
2612 + }
2613 + } else if (!resumableState.imageResources.hasOwnProperty(key)) {
2614 + // We must construct a new preload resource
2615 + resumableState.imageResources[key] = PRELOAD_NO_CREDS;
2616 + resource = [];
2617 + pushLinkImpl(
2618 + resource,
2619 + ({
2620 + rel: 'preload',
2621 + as: 'image',
2622 + // There is a bug in Safari where imageSrcSet is not respected on preload links
2623 + // so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
2624 + // This harms older browers that do not support imageSrcSet by making their preloads not work
2625 + // but this population is shrinking fast and is already small so we accept this tradeoff.
2626 + href: srcSet ? undefined : src,
2627 + imageSrcSet: srcSet,
2628 + imageSizes: sizes,
2629 + crossOrigin: props.crossOrigin,
2630 + integrity: props.integrity,
2631 + type: props.type,
2632 + fetchPriority: props.fetchPriority,
2633 + referrerPolicy: props.referrerPolicy,
2634 + }: PreloadProps),
2635 + );
2636 if (
2637 props.fetchPriority === 'high' ||
2638 renderState.highImagePreloads.size < 10
@@ -2565,6 +2640,9 @@ function pushImg(
2640 renderState.highImagePreloads.add(resource);
2641 } else {
2642 renderState.bulkPreloads.add(resource);
2643 + // We can bump the priority up if the same img is rendered later
2644 + // with fetchPriority="high"
2645 + promotablePreloads.set(key, resource);
2646 }
2647 }
2648 }
@@ -2899,33 +2977,48 @@ function pushScript(
2977 }
2978
2979 const src = props.src;
2902 - const key = getResourceKey('script', src);
2980 + const key = getResourceKey(src);
2981 // We can make this <script> into a ScriptResource
2904 - if (!resumableState.scriptsMap.hasOwnProperty(key)) {
2905 - const resource: ScriptResource = {
2906 - type: 'script',
2907 - chunks: [],
2908 - state: NoState,
2909 - props: null,
2910 - };
2911 - resumableState.scriptsMap[key] = null;
2912 - // Add to the script flushing queue
2913 - renderState.scripts.add(resource);
2982 +
2983 + let resources, preloads;
2984 + if (props.type === 'module') {
2985 + resources = resumableState.moduleScriptResources;
2986 + preloads = renderState.preloads.moduleScripts;
2987 + } else {
2988 + resources = resumableState.scriptResources;
2989 + preloads = renderState.preloads.scripts;
2990 + }
2991 +
2992 + const hasKey = resources.hasOwnProperty(key);
2993 + const resourceState = hasKey ? resources[key] : undefined;
2994 + if (resourceState !== EXISTS) {
2995 + // We are going to create this resource now so it is marked as Exists
2996 + resources[key] = EXISTS;
2997
2998 let scriptProps = props;
2916 - if (resumableState.preloadsMap.hasOwnProperty(key)) {
2917 - const preloadProps: PreloadProps = resumableState.preloadsMap[key];
2918 - scriptProps = {...props};
2919 - adoptPreloadPropsForScriptProps(scriptProps, preloadProps);
2920 - const preloadResource = renderState.preloadsMap.get(key);
2999 + if (resourceState) {
3000 + // When resourceState is truty it is a Preload state. We cast it for clarity
3001 + const preloadState: Preloaded | PreloadedWithCredentials =
3002 + resourceState;
3003 + if (preloadState.length === 2) {
3004 + scriptProps = {...props};
3005 + adoptPreloadCredentials(scriptProps, preloadState);
3006 + }
3007 +
3008 + const preloadResource = preloads.get(key);
3009 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;
3010 + // the preload resource exists was created in this render. Now that we have
3011 + // a script resource which will emit earlier than a preload would if it
3012 + // hasn't already flushed we prevent it from flushing by zeroing the length
3013 + preloadResource.length = 0;
3014 }
3015 }
3016 +
3017 + const resource: Resource = [];
3018 + // Add to the script flushing queue
3019 + renderState.scripts.add(resource);
3020 // encode the tag as Chunks
2928 - pushScriptImpl(resource.chunks, scriptProps);
3021 + pushScriptImpl(resource, scriptProps);
3022 }
3023
3024 if (textEmbedded) {
@@ -4130,51 +4223,56 @@ let destinationHasCapacity = true;
4223
4224 function flushStyleTagsLateForBoundary(
4225 this: Destination,
4133 - resource: StyleResource,
4226 + styleQueue: StyleQueue,
4227 ) {
4135 - if (
4136 - resource.type === 'stylesheet' &&
4137 - (resource.state & FlushedInPreamble) === NoState
4138 - ) {
4139 - currentlyRenderingBoundaryHasStylesToHoist = true;
4140 - } else if (resource.type === 'style') {
4141 - const chunks = resource.chunks;
4142 - const hrefs = resource.props.hrefs;
4143 - let i = 0;
4144 - if (chunks.length) {
4145 - writeChunk(this, lateStyleTagResourceOpen1);
4146 - writeChunk(
4147 - this,
4148 - stringToChunk(escapeTextForBrowser(resource.props.precedence)),
4149 - );
4150 - if (hrefs.length) {
4151 - writeChunk(this, lateStyleTagResourceOpen2);
4152 - for (; i < hrefs.length - 1; i++) {
4153 - writeChunk(this, stringToChunk(escapeTextForBrowser(hrefs[i])));
4154 - writeChunk(this, spaceSeparator);
4155 - }
4156 - writeChunk(this, stringToChunk(escapeTextForBrowser(hrefs[i])));
4157 - }
4158 - writeChunk(this, lateStyleTagResourceOpen3);
4159 - for (i = 0; i < chunks.length; i++) {
4160 - writeChunk(this, chunks[i]);
4161 - }
4162 - destinationHasCapacity = writeChunkAndReturn(
4163 - this,
4164 - lateStyleTagTemplateClose,
4228 + const rules = styleQueue.rules;
4229 + const hrefs = styleQueue.hrefs;
4230 + if (__DEV__) {
4231 + if (rules.length > 0 && hrefs.length === 0) {
4232 + console.error(
4233 + 'React expected to have at least one href for an a hoistable style but found none. This is a bug in React.',
4234 );
4235 + }
4236 + }
4237 + let i = 0;
4238 + if (hrefs.length) {
4239 + writeChunk(this, lateStyleTagResourceOpen1);
4240 + writeChunk(this, styleQueue.precedence);
4241 + writeChunk(this, lateStyleTagResourceOpen2);
4242 + for (; i < hrefs.length - 1; i++) {
4243 + writeChunk(this, hrefs[i]);
4244 + writeChunk(this, spaceSeparator);
4245 + }
4246 + writeChunk(this, hrefs[i]);
4247 + writeChunk(this, lateStyleTagResourceOpen3);
4248 + for (i = 0; i < rules.length; i++) {
4249 + writeChunk(this, rules[i]);
4250 + }
4251 + destinationHasCapacity = writeChunkAndReturn(
4252 + this,
4253 + lateStyleTagTemplateClose,
4254 + );
4255
4167 - // We wrote style tags for this boundary and we may need to emit a script
4168 - // to hoist them.
4169 - currentlyRenderingBoundaryHasStylesToHoist = true;
4256 + // We wrote style tags for this boundary and we may need to emit a script
4257 + // to hoist them.
4258 + currentlyRenderingBoundaryHasStylesToHoist = true;
4259
4171 - // style resources can flush continuously since more rules may be written into
4172 - // them with new hrefs. Instead of marking it flushed, we simply reset the chunks
4173 - // and hrefs
4174 - chunks.length = 0;
4175 - hrefs.length = 0;
4176 - }
4260 + // style resources can flush continuously since more rules may be written into
4261 + // them with new hrefs. Instead of marking it flushed, we simply reset the chunks
4262 + // and hrefs
4263 + rules.length = 0;
4264 + hrefs.length = 0;
4265 + }
4266 +}
4267 +
4268 +function hasStylesToHoist(stylesheet: StylesheetResource): boolean {
4269 + // We need to reveal boundaries with styles whenever a stylesheet it depends on is either
4270 + // not flushed or flushed after the preamble (shell).
4271 + if (stylesheet.state !== PREAMBLE) {
4272 + currentlyRenderingBoundaryHasStylesToHoist = true;
4273 + return true;
4274 }
4275 + return false;
4276 }
4277
4278 export function writeResourcesForBoundary(
@@ -4186,73 +4284,43 @@ export function writeResourcesForBoundary(
4284 currentlyRenderingBoundaryHasStylesToHoist = false;
4285 destinationHasCapacity = true;
4286
4189 - // Flush each Boundary resource
4190 - boundaryResources.forEach(flushStyleTagsLateForBoundary, destination);
4287 + // Flush style tags for each precedence this boundary depends on
4288 + boundaryResources.styles.forEach(flushStyleTagsLateForBoundary, destination);
4289 +
4290 + // Determine if this boundary has stylesheets that need to be awaited upon completion
4291 + boundaryResources.stylesheets.forEach(hasStylesToHoist);
4292 +
4293 if (currentlyRenderingBoundaryHasStylesToHoist) {
4294 renderState.stylesToHoist = true;
4295 }
4296 return destinationHasCapacity;
4297 }
4298
4197 -function flushResourceInPreamble<T: Resource>(this: Destination, resource: T) {
4198 - if ((resource.state & (Flushed | Blocked)) === NoState) {
4199 - const chunks = resource.chunks;
4200 - for (let i = 0; i < chunks.length; i++) {
4201 - writeChunk(this, chunks[i]);
4202 - }
4203 - resource.state |= FlushedInPreamble;
4204 - }
4205 -}
4206 -
4207 -function flushResourceLate<T: Resource>(this: Destination, resource: T) {
4208 - if ((resource.state & (Flushed | Blocked)) === NoState) {
4209 - const chunks = resource.chunks;
4210 - for (let i = 0; i < chunks.length; i++) {
4211 - writeChunk(this, chunks[i]);
4212 - }
4213 - resource.state |= FlushedLate;
4299 +function flushResource(this: Destination, resource: Resource) {
4300 + for (let i = 0; i < resource.length; i++) {
4301 + writeChunk(this, resource[i]);
4302 }
4303 + resource.length = 0;
4304 }
4305
4217 -// This must always be read after flushing stylesheet styles. we know we will encounter a style resource
4218 -// per precedence and it will be set before ready so we cast this to avoid an extra check at runtime
4219 -let precedenceStyleTagResource: StyleTagResource = (null: any);
4220 -
4221 -// This flags let's us opt out of flushing a placeholder style tag to emit the precedence in the right order.
4222 -// If a stylesheet was flushed then we have the precedence order preserved and only need to emit <style> tags
4223 -// if there are actual chunks to flush
4224 -let didFlushPrecedence = false;
4306 +const stylesheetFlushingQueue: Array<Chunk | PrecomputedChunk> = [];
4307
4308 function flushStyleInPreamble(
4309 this: Destination,
4228 - resource: StyleResource,
4310 + stylesheet: StylesheetResource,
4311 key: string,
4230 - map: Map<string, StyleResource>,
4312 + map: Map<string, StylesheetResource>,
4313 ) {
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.
4237 - map.delete(key);
4238 - } else {
4239 - // We can emit this style or stylesheet as is.
4240 - if (resource.type === 'style') {
4241 - precedenceStyleTagResource = resource;
4242 - return;
4243 - }
4244 -
4245 - // We still need to encode stylesheet chunks
4246 - // because unlike most Hoistables and Resources we do not eagerly encode
4247 - // them during render. This is because if we flush late we have to send a
4248 - // different encoding and we don't want to encode multiple times
4249 - pushLinkImpl(chunks, resource.props);
4250 - for (let i = 0; i < chunks.length; i++) {
4251 - writeChunk(this, chunks[i]);
4252 - }
4253 - resource.state |= FlushedInPreamble;
4254 - didFlushPrecedence = true;
4255 - }
4314 + // We still need to encode stylesheet chunks
4315 + // because unlike most Hoistables and Resources we do not eagerly encode
4316 + // them during render. This is because if we flush late we have to send a
4317 + // different encoding and we don't want to encode multiple times
4318 + pushLinkImpl(stylesheetFlushingQueue, stylesheet.props);
4319 + for (let i = 0; i < stylesheetFlushingQueue.length; i++) {
4320 + writeChunk(this, stylesheetFlushingQueue[i]);
4321 + }
4322 + stylesheetFlushingQueue.length = 0;
4323 + stylesheet.state = PREAMBLE;
4324 }
4325
4326 const styleTagResourceOpen1 = stringToPrecomputedChunk(
@@ -4264,74 +4332,64 @@ const styleTagResourceOpen3 = stringToPrecomputedChunk('">');
4332
4333 const styleTagResourceClose = stringToPrecomputedChunk('</style>');
4334
4267 -function flushAllStylesInPreamble(
4335 +function flushStylesInPreamble(
4336 this: Destination,
4269 - map: Map<string, StyleResource>,
4337 + styleQueue: StyleQueue,
4338 precedence: string,
4339 ) {
4272 - didFlushPrecedence = false;
4273 - map.forEach(flushStyleInPreamble, this);
4274 - map.clear();
4275 -
4276 - const chunks = precedenceStyleTagResource.chunks;
4277 - const hrefs = precedenceStyleTagResource.props.hrefs;
4278 - if (didFlushPrecedence === false || chunks.length) {
4340 + const hasStylesheets = styleQueue.sheets.size > 0;
4341 + styleQueue.sheets.forEach(flushStyleInPreamble, this);
4342 + styleQueue.sheets.clear();
4343 +
4344 + const rules = styleQueue.rules;
4345 + const hrefs = styleQueue.hrefs;
4346 + // If we don't emit any stylesheets at this precedence we still need to maintain the precedence
4347 + // order so even if there are no rules for style tags at this precedence we emit an empty style
4348 + // tag with the data-precedence attribute
4349 + if (!hasStylesheets || hrefs.length) {
4350 writeChunk(this, styleTagResourceOpen1);
4280 - writeChunk(this, stringToChunk(escapeTextForBrowser(precedence)));
4351 + writeChunk(this, styleQueue.precedence);
4352 let i = 0;
4353 if (hrefs.length) {
4354 writeChunk(this, styleTagResourceOpen2);
4355 for (; i < hrefs.length - 1; i++) {
4285 - writeChunk(this, stringToChunk(escapeTextForBrowser(hrefs[i])));
4356 + writeChunk(this, hrefs[i]);
4357 writeChunk(this, spaceSeparator);
4358 }
4288 - writeChunk(this, stringToChunk(escapeTextForBrowser(hrefs[i])));
4359 + writeChunk(this, hrefs[i]);
4360 }
4361 writeChunk(this, styleTagResourceOpen3);
4291 - for (i = 0; i < chunks.length; i++) {
4292 - writeChunk(this, chunks[i]);
4362 + for (i = 0; i < rules.length; i++) {
4363 + writeChunk(this, rules[i]);
4364 }
4365 writeChunk(this, styleTagResourceClose);
4366
4367 // style resources can flush continuously since more rules may be written into
4368 // them with new hrefs. Instead of marking it flushed, we simply reset the chunks
4369 // and hrefs
4299 - chunks.length = 0;
4370 + rules.length = 0;
4371 hrefs.length = 0;
4372 }
4373 }
4374
4304 -function preloadLateStyle(this: Destination, resource: StyleResource) {
4305 - if (resource.state & PreloadFlushed) {
4306 - // This resource has already had a preload flushed
4307 - return;
4308 - }
4309 -
4310 - if (resource.type === 'style') {
4311 - // <style> tags do not need to be preloaded
4312 - return;
4313 - }
4314 -
4315 - const chunks = resource.chunks;
4316 - const preloadProps = preloadAsStylePropsFromProps(
4317 - resource.props.href,
4318 - resource.props,
4319 - );
4320 - pushLinkImpl(chunks, preloadProps);
4321 - for (let i = 0; i < chunks.length; i++) {
4322 - writeChunk(this, chunks[i]);
4375 +function preloadLateStyle(this: Destination, stylesheet: StylesheetResource) {
4376 + if (stylesheet.state === PENDING) {
4377 + stylesheet.state = PRELOADED;
4378 + const preloadProps = preloadAsStylePropsFromProps(
4379 + stylesheet.props.href,
4380 + stylesheet.props,
4381 + );
4382 + pushLinkImpl(stylesheetFlushingQueue, preloadProps);
4383 + for (let i = 0; i < stylesheetFlushingQueue.length; i++) {
4384 + writeChunk(this, stylesheetFlushingQueue[i]);
4385 + }
4386 + stylesheetFlushingQueue.length = 0;
4387 }
4324 - resource.state |= PreloadFlushed;
4325 - chunks.length = 0;
4388 }
4389
4328 -function preloadLateStyles(
4329 - this: Destination,
4330 - map: Map<string, StyleResource>,
4331 - precedence: string,
4332 -) {
4333 - map.forEach(preloadLateStyle, this);
4334 - map.clear();
4390 +function preloadLateStyles(this: Destination, styleQueue: StyleQueue) {
4391 + styleQueue.sheets.forEach(preloadLateStyle, this);
4392 + styleQueue.sheets.clear();
4393 }
4394
4395 // We don't bother reporting backpressure at the moment because we expect to
@@ -4394,7 +4452,7 @@ export function writePreamble(
4452 charsetChunks.length = 0;
4453
4454 // emit preconnect resources
4397 - renderState.preconnects.forEach(flushResourceInPreamble, destination);
4455 + renderState.preconnects.forEach(flushResource, destination);
4456 renderState.preconnects.clear();
4457
4458 const preconnectChunks = renderState.preconnectChunks;
@@ -4403,14 +4461,14 @@ export function writePreamble(
4461 }
4462 preconnectChunks.length = 0;
4463
4406 - renderState.fontPreloads.forEach(flushResourceInPreamble, destination);
4464 + renderState.fontPreloads.forEach(flushResource, destination);
4465 renderState.fontPreloads.clear();
4466
4409 - renderState.highImagePreloads.forEach(flushResourceInPreamble, destination);
4467 + renderState.highImagePreloads.forEach(flushResource, destination);
4468 renderState.highImagePreloads.clear();
4469
4470 // Flush unblocked stylesheets by precedence
4413 - renderState.precedences.forEach(flushAllStylesInPreamble, destination);
4471 + renderState.styles.forEach(flushStylesInPreamble, destination);
4472
4473 const importMapChunks = renderState.importMapChunks;
4474 for (i = 0; i < importMapChunks.length; i++) {
@@ -4418,12 +4476,12 @@ export function writePreamble(
4476 }
4477 importMapChunks.length = 0;
4478
4421 - renderState.bootstrapScripts.forEach(flushResourceInPreamble, destination);
4479 + renderState.bootstrapScripts.forEach(flushResource, destination);
4480
4423 - renderState.scripts.forEach(flushResourceInPreamble, destination);
4481 + renderState.scripts.forEach(flushResource, destination);
4482 renderState.scripts.clear();
4483
4426 - renderState.bulkPreloads.forEach(flushResourceInPreamble, destination);
4484 + renderState.bulkPreloads.forEach(flushResource, destination);
4485 renderState.bulkPreloads.clear();
4486
4487 // Write embedding preloadChunks
@@ -4469,7 +4527,7 @@ export function writeHoistables(
4527 // We omit charsetChunks because we have already sent the shell and if it wasn't
4528 // already sent it is too late now.
4529
4472 - renderState.preconnects.forEach(flushResourceLate, destination);
4530 + renderState.preconnects.forEach(flushResource, destination);
4531 renderState.preconnects.clear();
4532
4533 const preconnectChunks = renderState.preconnectChunks;
@@ -4478,15 +4536,15 @@ export function writeHoistables(
4536 }
4537 preconnectChunks.length = 0;
4538
4481 - renderState.fontPreloads.forEach(flushResourceLate, destination);
4539 + renderState.fontPreloads.forEach(flushResource, destination);
4540 renderState.fontPreloads.clear();
4541
4484 - renderState.highImagePreloads.forEach(flushResourceInPreamble, destination);
4542 + renderState.highImagePreloads.forEach(flushResource, destination);
4543 renderState.highImagePreloads.clear();
4544
4545 // Preload any stylesheets. these will emit in a render instruction that follows this
4546 // but we want to kick off preloading as soon as possible
4489 - renderState.precedences.forEach(preloadLateStyles, destination);
4547 + renderState.styles.forEach(preloadLateStyles, destination);
4548
4549 // We only hoist importmaps that are configured through createResponse and that will
4550 // always flush in the preamble. Generally we don't expect people to render them as
@@ -4496,10 +4554,10 @@ export function writeHoistables(
4554 // bootstrap scripts should flush above script priority but these can only flush in the preamble
4555 // so we elide the code here for performance
4556
4499 - renderState.scripts.forEach(flushResourceLate, destination);
4557 + renderState.scripts.forEach(flushResource, destination);
4558 renderState.scripts.clear();
4559
4502 - renderState.bulkPreloads.forEach(flushResourceLate, destination);
4560 + renderState.bulkPreloads.forEach(flushResource, destination);
4561 renderState.bulkPreloads.clear();
4562
4563 // Write embedding preloadChunks
@@ -4548,13 +4606,11 @@ function writeStyleResourceDependenciesInJS(
4606 writeChunk(destination, arrayFirstOpenBracket);
4607
4608 let nextArrayOpenBrackChunk = arrayFirstOpenBracket;
4551 - boundaryResources.forEach(resource => {
4552 - if (resource.type === 'style') {
4553 - // Style dependencies don't require coordinated reveal and can be omitted
4554 - } else if (resource.state & FlushedInPreamble) {
4609 + boundaryResources.stylesheets.forEach(resource => {
4610 + if (resource.state === PREAMBLE) {
4611 // We can elide this dependency because it was flushed in the shell and
4612 // should be ready before content is shown on the client
4557 - } else if (resource.state & Flushed) {
4613 + } else if (resource.state === LATE) {
4614 // We only need to emit the href because this resource flushed in an earlier
4615 // boundary already which encoded the attributes necessary to construct
4616 // the resource instance on the client.
@@ -4565,7 +4621,7 @@ function writeStyleResourceDependenciesInJS(
4621 );
4622 writeChunk(destination, arrayCloseBracket);
4623 nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
4568 - } else if (resource.type === 'stylesheet') {
4624 + } else {
4625 // We need to emit the whole resource for insertion on the client
4626 writeChunk(destination, nextArrayOpenBrackChunk);
4627 writeStyleResourceDependencyInJS(
@@ -4577,7 +4633,7 @@ function writeStyleResourceDependenciesInJS(
4633 writeChunk(destination, arrayCloseBracket);
4634 nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
4635
4580 - resource.state |= FlushedLate;
4636 + resource.state = LATE;
4637 }
4638 });
4639 writeChunk(destination, arrayCloseBracket);
@@ -4743,13 +4799,11 @@ function writeStyleResourceDependenciesInAttr(
4799 writeChunk(destination, arrayFirstOpenBracket);
4800
4801 let nextArrayOpenBrackChunk = arrayFirstOpenBracket;
4746 - boundaryResources.forEach(resource => {
4747 - if (resource.type === 'style') {
4748 - // Style dependencies don't require coordinated reveal and can be omitted
4749 - } else if (resource.state & FlushedInPreamble) {
4802 + boundaryResources.stylesheets.forEach(resource => {
4803 + if (resource.state === PREAMBLE) {
4804 // We can elide this dependency because it was flushed in the shell and
4805 // should be ready before content is shown on the client
4752 - } else if (resource.state & Flushed) {
4806 + } else if (resource.state === LATE) {
4807 // We only need to emit the href because this resource flushed in an earlier
4808 // boundary already which encoded the attributes necessary to construct
4809 // the resource instance on the client.
@@ -4760,7 +4814,7 @@ function writeStyleResourceDependenciesInAttr(
4814 );
4815 writeChunk(destination, arrayCloseBracket);
4816 nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
4763 - } else if (resource.type === 'stylesheet') {
4817 + } else {
4818 // We need to emit the whole resource for insertion on the client
4819 writeChunk(destination, nextArrayOpenBrackChunk);
4820 writeStyleResourceDependencyInAttr(
@@ -4772,7 +4826,7 @@ function writeStyleResourceDependenciesInAttr(
4826 writeChunk(destination, arrayCloseBracket);
4827 nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
4828
4775 - resource.state |= FlushedLate;
4829 + resource.state = LATE;
4830 }
4831 });
4832 writeChunk(destination, arrayCloseBracket);
@@ -4933,35 +4987,17 @@ function writeStyleResourceAttributeInAttr(
4987 * Resources
4988 */
4989
4936 -type ResourceStateTag = number;
4937 -const NoState /* */ = 0b0000;
4938 -// These tags indicate whether the Resource was flushed and in which phase
4939 -const FlushedInPreamble /* */ = 0b0001;
4940 -const FlushedLate /* */ = 0b0010;
4941 -const Flushed /* */ = 0b0011;
4942 -// This tag indicates whether this Resource is blocked from flushing.
4943 -// This currently is only used with stylesheets that are blocked by a Boundary
4944 -const Blocked /* */ = 0b0100;
4945 -// This tag indicates whether this Resource has been preloaded.
4946 -// This generally only makes sense for Resources other than PreloadResource
4947 -const PreloadFlushed /* */ = 0b1000;
4948 -
4949 -type TResource<
4950 - T: 'stylesheet' | 'style' | 'script' | 'preload' | 'preconnect',
4951 - P,
4952 -> = {
4953 - type: T,
4954 - chunks: Array<Chunk | PrecomputedChunk>,
4955 - state: ResourceStateTag,
4956 - props: P,
4957 -};
4990 +type StylesheetState = 0 | 1 | 2 | 3;
4991 +const PENDING: StylesheetState = 0;
4992 +const PRELOADED: StylesheetState = 1;
4993 +const PREAMBLE: StylesheetState = 2;
4994 +const LATE: StylesheetState = 3;
4995
4996 type PreconnectProps = {
4997 rel: 'preconnect' | 'dns-prefetch',
4998 href: string,
4999 [string]: mixed,
5000 };
4964 -type PreconnectResource = TResource<'preconnect', null>;
5001
5002 type PreloadAsProps = {
5003 rel: 'preload',
@@ -4975,47 +5011,49 @@ type PreloadModuleProps = {
5011 [string]: ?string,
5012 };
5013 type PreloadProps = PreloadAsProps | PreloadModuleProps;
4978 -type PreloadResource = TResource<'preload', PreloadProps>;
4979 -
4980 -type StylesheetProps = {
4981 - rel: 'stylesheet',
4982 - href: string,
4983 - 'data-precedence': string,
4984 - [string]: mixed,
4985 -};
4986 -type StylesheetResource = TResource<'stylesheet', StylesheetProps>;
4987 -
4988 -type StyleTagProps = {
4989 - hrefs: Array<string>,
4990 - precedence: string,
4991 -};
4992 -type StyleTagResource = TResource<'style', StyleTagProps>;
4993 -
4994 -type StyleResource = StylesheetResource | StyleTagResource;
5014
5015 type ScriptProps = {
5016 async: true,
5017 src: string,
5018 [string]: mixed,
5019 };
5001 -type ModuleProps = {
5020 +type ModuleScriptProps = {
5021 async: true,
5022 src: string,
5023 type: 'module',
5024 [string]: mixed,
5025 };
5007 -type ScriptResource = TResource<'script', null>;
5026
5009 -type Resource =
5010 - | StyleResource
5011 - | ScriptResource
5012 - | PreloadResource
5013 - | PreconnectResource;
5027 +export type Resource = Array<Chunk | PrecomputedChunk>;
5028 +
5029 +type StylesheetProps = {
5030 + rel: 'stylesheet',
5031 + href: string,
5032 + 'data-precedence': string,
5033 + [string]: mixed,
5034 +};
5035 +type StylesheetResource = {
5036 + props: StylesheetProps,
5037 + state: StylesheetState,
5038 +};
5039 +
5040 +export type BoundaryResources = {
5041 + styles: Set<StyleQueue>,
5042 + stylesheets: Set<StylesheetResource>,
5043 +};
5044
5015 -export type BoundaryResources = Set<StyleResource>;
5045 +export type StyleQueue = {
5046 + precedence: Chunk | PrecomputedChunk,
5047 + rules: Array<Chunk | PrecomputedChunk>,
5048 + hrefs: Array<Chunk | PrecomputedChunk>,
5049 + sheets: Map<string, StylesheetResource>,
5050 +};
5051
5052 export function createBoundaryResources(): BoundaryResources {
5018 - return new Set();
5053 + return {
5054 + styles: new Set(),
5055 + stylesheets: new Set(),
5056 + };
5057 }
5058
5059 export function setCurrentlyRenderingBoundaryResourcesTarget(
@@ -5025,8 +5063,19 @@ export function setCurrentlyRenderingBoundaryResourcesTarget(
5063 renderState.boundaryResources = boundaryResources;
5064 }
5065
5028 -function getResourceKey(as: string, href: string): string {
5029 - return `[${as}]${href}`;
5066 +function getResourceKey(href: string): string {
5067 + return href;
5068 +}
5069 +
5070 +function getImageResourceKey(
5071 + href: string,
5072 + imageSrcSet?: ?string,
5073 + imageSizes?: ?string,
5074 +): string {
5075 + if (imageSrcSet) {
5076 + return imageSrcSet + '\n' + (imageSizes || '');
5077 + }
5078 + return href;
5079 }
5080
5081 function prefetchDNS(href: string) {
@@ -5046,19 +5095,11 @@ function prefetchDNS(href: string) {
5095 const renderState = getRenderState(request);
5096
5097 if (typeof href === 'string' && href) {
5049 - const key = getResourceKey('prefetchDNS', href);
5050 - if (!resumableState.preconnectsMap.hasOwnProperty(key)) {
5051 - const resource: PreconnectResource = {
5052 - type: 'preconnect',
5053 - chunks: [],
5054 - state: NoState,
5055 - props: null,
5056 - };
5057 - resumableState.preconnectsMap[key] = null;
5058 - pushLinkImpl(
5059 - resource.chunks,
5060 - ({href, rel: 'dns-prefetch'}: PreconnectProps),
5061 - );
5098 + const key = getResourceKey(href);
5099 + if (!resumableState.dnsResources.hasOwnProperty(key)) {
5100 + const resource: Resource = [];
5101 + resumableState.dnsResources[key] = EXISTS;
5102 + pushLinkImpl(resource, ({href, rel: 'dns-prefetch'}: PreconnectProps));
5103 renderState.preconnects.add(resource);
5104 }
5105 flushResources(request);
@@ -5082,19 +5123,18 @@ function preconnect(href: string, crossOrigin: ?CrossOriginEnum) {
5123 const renderState = getRenderState(request);
5124
5125 if (typeof href === 'string' && href) {
5085 - const key = `[preconnect][${
5086 - typeof crossOrigin === 'string' ? crossOrigin : 'null'
5087 - }]${href}`;
5088 - if (!resumableState.preconnectsMap.hasOwnProperty(key)) {
5089 - const resource: PreconnectResource = {
5090 - type: 'preconnect',
5091 - chunks: [],
5092 - state: NoState,
5093 - props: null,
5094 - };
5095 - resumableState.preconnectsMap[key] = null;
5126 + const resources =
5127 + crossOrigin === 'use-credentials'
5128 + ? resumableState.connectResources.credentials
5129 + : typeof crossOrigin === 'string'
5130 + ? resumableState.connectResources.anonymous
5131 + : resumableState.connectResources.default;
5132 + const key = getResourceKey(href);
5133 + if (!resources.hasOwnProperty(key)) {
5134 + const resource: Resource = [];
5135 + resources[key] = EXISTS;
5136 pushLinkImpl(
5097 - resource.chunks,
5137 + resource,
5138 ({rel: 'preconnect', href, crossOrigin}: PreconnectProps),
5139 );
5140 renderState.preconnects.add(resource);
@@ -5119,42 +5159,124 @@ function preload(href: string, as: string, options?: ?PreloadImplOptions) {
5159 const resumableState = getResumableState(request);
5160 const renderState = getRenderState(request);
5161 if (as && href) {
5122 - options = options || {};
5123 - let key: string;
5124 - if (as === 'image') {
5125 - // For image preloads the key contains either the imageSrcSet + imageSizes or the href but not
5126 - // both. This is to prevent identical calls with the same srcSet and sizes to be duplicated
5127 - // by varying the href. this is an edge case but it is the most correct behavior.
5128 - key = getImagePreloadKey(href, options.imageSrcSet, options.imageSizes);
5129 - } else {
5130 - key = getResourceKey(as, href);
5131 - }
5132 - if (!resumableState.preloadsMap.hasOwnProperty(key)) {
5133 - const props = Object.assign(
5134 - ({
5135 - rel: 'preload',
5136 - href: as === 'image' && options.imageSrcSet ? undefined : href,
5137 - as,
5138 - }: PreloadProps),
5139 - options,
5140 - );
5141 - const resource: PreloadResource = {
5142 - type: 'preload',
5143 - chunks: [],
5144 - state: NoState,
5145 - props,
5146 - };
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 {
5162 + switch (as) {
5163 + case 'image': {
5164 + let imageSrcSet, imageSizes, fetchPriority;
5165 + if (options) {
5166 + imageSrcSet = options.imageSrcSet;
5167 + imageSizes = options.imageSizes;
5168 + fetchPriority = options.fetchPriority;
5169 + }
5170 + const key = getImageResourceKey(href, imageSrcSet, imageSizes);
5171 + if (resumableState.imageResources.hasOwnProperty(key)) {
5172 + // we can return if we already have this resource
5173 + return;
5174 + }
5175 + resumableState.imageResources[key] = PRELOAD_NO_CREDS;
5176 + const resource = ([]: Resource);
5177 + pushLinkImpl(
5178 + resource,
5179 + Object.assign(
5180 + ({
5181 + rel: 'preload',
5182 + // There is a bug in Safari where imageSrcSet is not respected on preload links
5183 + // so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
5184 + // This harms older browers that do not support imageSrcSet by making their preloads not work
5185 + // but this population is shrinking fast and is already small so we accept this tradeoff.
5186 + href: imageSrcSet ? undefined : href,
5187 + as,
5188 + }: PreloadAsProps),
5189 + options,
5190 + ),
5191 + );
5192 + if (fetchPriority === 'high') {
5193 + renderState.highImagePreloads.add(resource);
5194 + } else {
5195 + renderState.bulkPreloads.add(resource);
5196 + // Stash the resource in case we need to promote it to higher priority
5197 + // when an img tag is rendered
5198 + renderState.preloads.images.set(key, resource);
5199 + }
5200 + break;
5201 + }
5202 + case 'style': {
5203 + const key = getResourceKey(href);
5204 + if (resumableState.styleResources.hasOwnProperty(key)) {
5205 + // we can return if we already have this resource
5206 + return;
5207 + }
5208 + const resource = ([]: Resource);
5209 + pushLinkImpl(
5210 + resource,
5211 + Object.assign(({rel: 'preload', href, as}: PreloadAsProps), options),
5212 + );
5213 + resumableState.styleResources[key] =
5214 + options &&
5215 + (typeof options.crossOrigin === 'string' ||
5216 + typeof options.integrity === 'string')
5217 + ? [options.crossOrigin, options.integrity]
5218 + : PRELOAD_NO_CREDS;
5219 + renderState.preloads.stylesheets.set(key, resource);
5220 renderState.bulkPreloads.add(resource);
5221 + break;
5222 + }
5223 + case 'script': {
5224 + const key = getResourceKey(href);
5225 + if (resumableState.scriptResources.hasOwnProperty(key)) {
5226 + // we can return if we already have this resource
5227 + return;
5228 + }
5229 + const resource = ([]: Resource);
5230 + renderState.preloads.scripts.set(key, resource);
5231 + renderState.bulkPreloads.add(resource);
5232 + pushLinkImpl(
5233 + resource,
5234 + Object.assign(({rel: 'preload', href, as}: PreloadAsProps), options),
5235 + );
5236 + resumableState.scriptResources[key] =
5237 + options &&
5238 + (typeof options.crossOrigin === 'string' ||
5239 + typeof options.integrity === 'string')
5240 + ? [options.crossOrigin, options.integrity]
5241 + : PRELOAD_NO_CREDS;
5242 + break;
5243 + }
5244 + default: {
5245 + const key = getResourceKey(href);
5246 + const hasAsType = resumableState.unknownResources.hasOwnProperty(as);
5247 + let resources;
5248 + if (hasAsType) {
5249 + resources = resumableState.unknownResources[as];
5250 + if (resources.hasOwnProperty(key)) {
5251 + // we can return if we already have this resource
5252 + return;
5253 + }
5254 + } else {
5255 + resources = ({}: ResumableState['unknownResources']['asType']);
5256 + resumableState.unknownResources[as] = resources;
5257 + }
5258 + const resource = ([]: Resource);
5259 + const props = Object.assign(
5260 + ({
5261 + rel: 'preload',
5262 + href,
5263 + as,
5264 + }: PreloadAsProps),
5265 + options,
5266 + );
5267 + switch (as) {
5268 + case 'font':
5269 + renderState.fontPreloads.add(resource);
5270 + break;
5271 + // intentional fall through
5272 + default:
5273 + renderState.bulkPreloads.add(resource);
5274 + }
5275 + pushLinkImpl(resource, props);
5276 + resources[key] = PRELOAD_NO_CREDS;
5277 }
5278 }
5279 + // If we got this far we created a new resource
5280 flushResources(request);
5281 }
5282 }
@@ -5178,28 +5300,58 @@ function preloadModule(
5300 const resumableState = getResumableState(request);
5301 const renderState = getRenderState(request);
5302 if (href) {
5303 + const key = getResourceKey(href);
5304 const as =
5305 options && typeof options.as === 'string' ? options.as : 'script';
5183 - const key = getResourceKey(as, href);
5184 - if (!resumableState.preloadsMap.hasOwnProperty(key)) {
5185 - const props: PreloadModuleProps = Object.assign(
5306 +
5307 + let resource;
5308 + switch (as) {
5309 + case 'script': {
5310 + if (resumableState.moduleScriptResources.hasOwnProperty(key)) {
5311 + // we can return if we already have this resource
5312 + return;
5313 + }
5314 + resource = ([]: Resource);
5315 + resumableState.moduleScriptResources[key] =
5316 + options &&
5317 + (typeof options.crossOrigin === 'string' ||
5318 + typeof options.integrity === 'string')
5319 + ? [options.crossOrigin, options.integrity]
5320 + : PRELOAD_NO_CREDS;
5321 + renderState.preloads.moduleScripts.set(key, resource);
5322 + break;
5323 + }
5324 + default: {
5325 + const hasAsType =
5326 + resumableState.moduleUnknownResources.hasOwnProperty(as);
5327 + let resources;
5328 + if (hasAsType) {
5329 + resources = resumableState.unknownResources[as];
5330 + if (resources.hasOwnProperty(key)) {
5331 + // we can return if we already have this resource
5332 + return;
5333 + }
5334 + } else {
5335 + resources = ({}: ResumableState['moduleUnknownResources']['asType']);
5336 + resumableState.moduleUnknownResources[as] = resources;
5337 + }
5338 + resource = ([]: Resource);
5339 + resources[key] = PRELOAD_NO_CREDS;
5340 + }
5341 + }
5342 +
5343 + pushLinkImpl(
5344 + resource,
5345 + Object.assign(
5346 ({
5347 rel: 'modulepreload',
5348 href,
5349 }: PreloadModuleProps),
5350 options,
5191 - );
5192 - const resource: PreloadResource = {
5193 - type: 'preload',
5194 - chunks: [],
5195 - state: NoState,
5196 - props,
5197 - };
5198 - resumableState.preloadsMap[key] = props;
5199 - renderState.preloadsMap.set(key, resource);
5200 - pushLinkImpl(resource.chunks, resource.props);
5201 - renderState.bulkPreloads.add(resource);
5202 - }
5351 + ),
5352 + );
5353 + renderState.bulkPreloads.add(resource);
5354 + // If we got this far we created a new resource
5355 flushResources(request);
5356 }
5357 }
@@ -5224,62 +5376,74 @@ function preinitStyle(
5376 const resumableState = getResumableState(request);
5377 const renderState = getRenderState(request);
5378 if (href) {
5227 - const as = 'style';
5228 - const key = getResourceKey(as, href);
5229 - if (!resumableState.stylesMap.hasOwnProperty(key)) {
5230 - precedence = precedence || 'default';
5231 - let state = NoState;
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 - ({
5242 - rel: 'stylesheet',
5243 - href,
5244 - 'data-precedence': precedence,
5245 - }: StylesheetProps),
5246 - options,
5247 - );
5379 + precedence = precedence || 'default';
5380 + const key = getResourceKey(href);
5381 +
5382 + let styleQueue = renderState.styles.get(precedence);
5383 + const hasKey = resumableState.styleResources.hasOwnProperty(key);
5384 + const resourceState = hasKey
5385 + ? resumableState.styleResources[key]
5386 + : undefined;
5387 + if (resourceState !== EXISTS) {
5388 + // We are going to create this resource now so it is marked as Exists
5389 + resumableState.styleResources[key] = EXISTS;
5390 +
5391 + // If this is the first time we've encountered this precedence we need
5392 + // to create a StyleQueue
5393 + if (!styleQueue) {
5394 + styleQueue = {
5395 + precedence: stringToChunk(escapeTextForBrowser(precedence)),
5396 + rules: ([]: Array<Chunk | PrecomputedChunk>),
5397 + hrefs: ([]: Array<Chunk | PrecomputedChunk>),
5398 + sheets: (new Map(): Map<string, StylesheetResource>),
5399 + };
5400 + renderState.styles.set(precedence, styleQueue);
5401 + }
5402 +
5403 const resource = {
5249 - type: 'stylesheet',
5250 - chunks: ([]: Array<Chunk | PrecomputedChunk>),
5251 - state,
5252 - props,
5404 + state: PENDING,
5405 + props: Object.assign(
5406 + ({
5407 + rel: 'stylesheet',
5408 + href,
5409 + 'data-precedence': precedence,
5410 + }: StylesheetProps),
5411 + options,
5412 + ),
5413 };
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>),
5262 - state: NoState,
5263 - props: {
5264 - precedence,
5265 - hrefs: ([]: Array<string>),
5266 - },
5267 - };
5268 - stylesInPrecedence.set('', emptyStyleResource);
5269 - if (__DEV__) {
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 - }
5414 +
5415 + if (resourceState) {
5416 + // When resourceState is truty it is a Preload state. We cast it for clarity
5417 + const preloadState: Preloaded | PreloadedWithCredentials =
5418 + resourceState;
5419 + if (preloadState.length === 2) {
5420 + adoptPreloadCredentials(resource.props, preloadState);
5421 }
5277 - renderState.stylePrecedences.set(precedence, emptyStyleResource);
5422 +
5423 + const preloadResource = renderState.preloads.stylesheets.get(key);
5424 + if (preloadResource && preloadResource.length > 0) {
5425 + // The Preload for this resource was created in this render pass and has not flushed yet so
5426 + // we need to clear it to avoid it flushing.
5427 + preloadResource.length = 0;
5428 + } else {
5429 + // Either the preload resource from this render already flushed in this render pass
5430 + // or the preload flushed in a prior pass (prerender). In either case we need to mark
5431 + // this resource as already having been preloaded.
5432 + resource.state = PRELOADED;
5433 + }
5434 + } else {
5435 + // We don't need to check whether a preloadResource exists in the renderState
5436 + // because if it did exist then the resourceState would also exist and we would
5437 + // have hit the primary if condition above.
5438 }
5279 - stylesInPrecedence.set(key, resource);
5439 +
5440 + // We add the newly created resource to our StyleQueue and if necessary
5441 + // track the resource with the currently rendering boundary
5442 + styleQueue.sheets.set(key, resource);
5443 +
5444 + // Notify the request that there are resources to flush even if no work is currently happening
5445 flushResources(request);
5446 }
5282 - return;
5447 }
5448 }
5449
@@ -5299,15 +5463,16 @@ function preinitScript(src: string, options?: ?PreinitScriptOptions): void {
5463 const resumableState = getResumableState(request);
5464 const renderState = getRenderState(request);
5465 if (src) {
5302 - const key = getResourceKey('script', src);
5303 - if (!resumableState.scriptsMap.hasOwnProperty(key)) {
5304 - const resource: ScriptResource = {
5305 - type: 'script',
5306 - chunks: [],
5307 - state: NoState,
5308 - props: null,
5309 - };
5310 - resumableState.scriptsMap[key] = null;
5466 + const key = getResourceKey(src);
5467 +
5468 + const hasKey = resumableState.scriptResources.hasOwnProperty(key);
5469 + const resourceState = hasKey
5470 + ? resumableState.scriptResources[key]
5471 + : undefined;
5472 + if (resourceState !== EXISTS) {
5473 + // We are going to create this resource now so it is marked as Exists
5474 + resumableState.scriptResources[key] = EXISTS;
5475 +
5476 const props: ScriptProps = Object.assign(
5477 ({
5478 src,
@@ -5315,8 +5480,29 @@ function preinitScript(src: string, options?: ?PreinitScriptOptions): void {
5480 }: ScriptProps),
5481 options,
5482 );
5483 + if (resourceState) {
5484 + // When resourceState is truty it is a Preload state. We cast it for clarity
5485 + const preloadState: Preloaded | PreloadedWithCredentials =
5486 + resourceState;
5487 + if (preloadState.length === 2) {
5488 + adoptPreloadCredentials(props, preloadState);
5489 + }
5490 +
5491 + const preloadResource = renderState.preloads.scripts.get(key);
5492 + if (preloadResource) {
5493 + // the preload resource exists was created in this render. Now that we have
5494 + // a script resource which will emit earlier than a preload would if it
5495 + // hasn't already flushed we prevent it from flushing by zeroing the length
5496 + preloadResource.length = 0;
5497 + }
5498 + }
5499 +
5500 + const resource: Resource = [];
5501 + // Add to the script flushing queue
5502 renderState.scripts.add(resource);
5319 - pushScriptImpl(resource.chunks, props);
5503 + // encode the tag as Chunks
5504 + pushScriptImpl(resource, props);
5505 + // Notify the request that there are resources to flush even if no work is currently happening
5506 flushResources(request);
5507 }
5508 return;
@@ -5342,25 +5528,46 @@ function preinitModuleScript(
5528 const resumableState = getResumableState(request);
5529 const renderState = getRenderState(request);
5530 if (src) {
5345 - const key = getResourceKey('script', src);
5346 - if (!resumableState.scriptsMap.hasOwnProperty(key)) {
5347 - const resource: ScriptResource = {
5348 - type: 'script',
5349 - chunks: [],
5350 - state: NoState,
5351 - props: null,
5352 - };
5353 - resumableState.scriptsMap[key] = null;
5531 + const key = getResourceKey(src);
5532 + const hasKey = resumableState.moduleScriptResources.hasOwnProperty(key);
5533 + const resourceState = hasKey
5534 + ? resumableState.moduleScriptResources[key]
5535 + : undefined;
5536 + if (resourceState !== EXISTS) {
5537 + // We are going to create this resource now so it is marked as Exists
5538 + resumableState.moduleScriptResources[key] = EXISTS;
5539 +
5540 const props = Object.assign(
5541 ({
5542 src,
5543 type: 'module',
5544 async: true,
5359 - }: ModuleProps),
5545 + }: ModuleScriptProps),
5546 options,
5547 );
5548 + if (resourceState) {
5549 + // When resourceState is truty it is a Preload state. We cast it for clarity
5550 + const preloadState: Preloaded | PreloadedWithCredentials =
5551 + resourceState;
5552 + if (preloadState.length === 2) {
5553 + adoptPreloadCredentials(props, preloadState);
5554 + }
5555 +
5556 + const preloadResource = renderState.preloads.moduleScripts.get(key);
5557 + if (preloadResource) {
5558 + // the preload resource exists was created in this render. Now that we have
5559 + // a script resource which will emit earlier than a preload would if it
5560 + // hasn't already flushed we prevent it from flushing by zeroing the length
5561 + preloadResource.length = 0;
5562 + }
5563 + }
5564 +
5565 + const resource: Resource = [];
5566 + // Add to the script flushing queue
5567 renderState.scripts.add(resource);
5363 - pushScriptImpl(resource.chunks, props);
5568 + // encode the tag as Chunks
5569 + pushScriptImpl(resource, props);
5570 + // Notify the request that there are resources to flush even if no work is currently happening
5571 flushResources(request);
5572 }
5573 return;
@@ -5368,93 +5575,46 @@ function preinitModuleScript(
5575 }
5576
5577 // This function is only safe to call at Request start time since it assumes
5371 -// that each script has not already been preloaded. If we find a need to preload
5578 +// that each module has not already been preloaded. If we find a need to preload
5579 // scripts at any other point in time we will need to check whether the preload
5580 // already exists and not assume it
5374 -function preloadBootstrapScript(
5581 +function preloadBootstrapScriptOrModule(
5582 resumableState: ResumableState,
5583 renderState: RenderState,
5377 - src: string,
5378 - nonce: ?string,
5379 - integrity: ?string,
5380 - crossOrigin: ?string,
5584 + href: string,
5585 + props: PreloadProps,
5586 ): void {
5382 - const key = getResourceKey('script', src);
5383 - if (__DEV__) {
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
5388 - console.error(
5389 - 'Internal React Error: React expected bootstrap script with src "%s" to not have been preloaded already. please file an issue',
5390 - src,
5391 - );
5392 - }
5587 + if (!enableFloat) {
5588 + return;
5589 }
5394 - const props: PreloadProps = {
5395 - rel: 'preload',
5396 - href: src,
5397 - as: 'script',
5398 - fetchPriority: 'low',
5399 - nonce,
5400 - integrity,
5401 - crossOrigin,
5402 - };
5403 - const resource: PreloadResource = {
5404 - type: 'preload',
5405 - chunks: [],
5406 - state: NoState,
5407 - props,
5408 - };
5409 - resumableState.preloadsMap[key] = props;
5410 - renderState.preloadsMap.set(key, resource);
5411 - renderState.bootstrapScripts.add(resource);
5412 - pushLinkImpl(resource.chunks, props);
5413 -}
5590 + const key = getResourceKey(href);
5591
5415 -// This function is only safe to call at Request start time since it assumes
5416 -// that each module has not already been preloaded. If we find a need to preload
5417 -// scripts at any other point in time we will need to check whether the preload
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,
5425 - crossOrigin: ?string,
5426 -): void {
5427 - const key = getResourceKey('script', src);
5592 if (__DEV__) {
5429 - if (resumableState.preloadsMap.hasOwnProperty(key)) {
5593 + if (
5594 + resumableState.scriptResources.hasOwnProperty(key) ||
5595 + resumableState.moduleScriptResources.hasOwnProperty(key)
5596 + ) {
5597 // This is coded as a React error because it should be impossible for a userspace preload to preempt this call
5598 // If a userspace preload can preempt it then this assumption is broken and we need to reconsider this strategy
5599 // rather than instruct the user to not preload their bootstrap scripts themselves
5600 console.error(
5434 - 'Internal React Error: React expected bootstrap module with src "%s" to not have been preloaded already. please file an issue',
5435 - src,
5601 + 'Internal React Error: React expected bootstrap script or module with src "%s" to not have been preloaded already. please file an issue',
5602 + href,
5603 );
5604 }
5605 }
5439 - const props: PreloadModuleProps = {
5440 - rel: 'modulepreload',
5441 - href: src,
5442 - fetchPriority: 'low',
5443 - nonce,
5444 - integrity,
5445 - crossOrigin,
5446 - };
5447 - const resource: PreloadResource = {
5448 - type: 'preload',
5449 - chunks: [],
5450 - state: NoState,
5451 - props,
5452 - };
5453 - resumableState.preloadsMap[key] = props;
5454 - renderState.preloadsMap.set(key, resource);
5606 +
5607 + // The href used for bootstrap scripts and bootstrap modules should never be
5608 + // used to preinit the resource. If a script can be preinited then it shouldn't
5609 + // be a bootstrap script/module and if it is a bootstrap script/module then it
5610 + // must not be safe to emit early. To avoid possibly allowing for preinits of
5611 + // bootstrap scripts/modules we occlude these keys.
5612 + resumableState.scriptResources[key] = EXISTS;
5613 + resumableState.moduleScriptResources[key] = EXISTS;
5614 +
5615 + const resource: Resource = [];
5616 + pushLinkImpl(resource, props);
5617 renderState.bootstrapScripts.add(resource);
5456 - pushLinkImpl(resource.chunks, props);
5457 - return;
5618 }
5619
5620 function internalPreinitScript(
@@ -5463,15 +5623,10 @@ function internalPreinitScript(
5623 src: string,
5624 chunks: Array<Chunk | PrecomputedChunk>,
5625 ): void {
5466 - const key = getResourceKey('script', src);
5467 - if (!resumableState.scriptsMap.hasOwnProperty(key)) {
5468 - const resource: ScriptResource = {
5469 - type: 'script',
5470 - chunks,
5471 - state: NoState,
5472 - props: null,
5473 - };
5474 - resumableState.scriptsMap[key] = null;
5626 + const key = getResourceKey(src);
5627 + if (!resumableState.scriptResources.hasOwnProperty(key)) {
5628 + const resource: Resource = chunks;
5629 + resumableState.scriptResources[key] = EXISTS;
5630 renderState.scripts.add(resource);
5631 }
5632 return;
@@ -5499,28 +5654,26 @@ function stylesheetPropsFromRawProps(rawProps: any): StylesheetProps {
5654 };
5655 }
5656
5502 -function adoptPreloadPropsForStylesheetProps(
5503 - resourceProps: StylesheetProps,
5504 - preloadProps: PreloadProps,
5657 +function adoptPreloadCredentials(
5658 + target: StylesheetProps | ScriptProps | ModuleScriptProps,
5659 + preloadState: PreloadedWithCredentials,
5660 ): void {
5506 - if (resourceProps.crossOrigin == null)
5507 - resourceProps.crossOrigin = preloadProps.crossOrigin;
5508 - if (resourceProps.integrity == null)
5509 - resourceProps.integrity = preloadProps.integrity;
5661 + if (target.crossOrigin == null) target.crossOrigin = preloadState[0];
5662 + if (target.integrity == null) target.integrity = preloadState[1];
5663 }
5664
5512 -function adoptPreloadPropsForScriptProps(
5513 - resourceProps: ScriptProps,
5514 - preloadProps: PreloadProps,
5515 -): void {
5516 - if (resourceProps.crossOrigin == null)
5517 - resourceProps.crossOrigin = preloadProps.crossOrigin;
5518 - if (resourceProps.integrity == null)
5519 - resourceProps.integrity = preloadProps.integrity;
5665 +function hoistStyleQueueDependency(
5666 + this: BoundaryResources,
5667 + styleQueue: StyleQueue,
5668 +) {
5669 + this.styles.add(styleQueue);
5670 }
5671
5522 -function hoistStyleResource(this: BoundaryResources, resource: StyleResource) {
5523 - this.add(resource);
5672 +function hoistStylesheetDependency(
5673 + this: BoundaryResources,
5674 + stylesheet: StylesheetResource,
5675 +) {
5676 + this.stylesheets.add(stylesheet);
5677 }
5678
5679 export function hoistResources(
@@ -5529,7 +5682,11 @@ export function hoistResources(
5682 ): void {
5683 const currentBoundaryResources = renderState.boundaryResources;
5684 if (currentBoundaryResources) {
5532 - source.forEach(hoistStyleResource, currentBoundaryResources);
5685 + source.styles.forEach(hoistStyleQueueDependency, currentBoundaryResources);
5686 + source.stylesheets.forEach(
5687 + hoistStylesheetDependency,
5688 + currentBoundaryResources,
5689 + );
5690 }
5691 }
5692
packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js
+22 -14
@@ -7,7 +7,12 @@
7 * @flow
8 */
9
10 -import type {ResumableState, BoundaryResources} from './ReactFizzConfigDOM';
10 +import type {
11 + ResumableState,
12 + BoundaryResources,
13 + StyleQueue,
14 + Resource,
15 +} from './ReactFizzConfigDOM';
16
17 import {
18 createRenderState as createRenderStateImpl,
@@ -46,16 +51,20 @@ export type RenderState = {
51 importMapChunks: Array<Chunk | PrecomputedChunk>,
52 preloadChunks: Array<Chunk | PrecomputedChunk>,
53 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>,
54 + preconnects: Set<Resource>,
55 + fontPreloads: Set<Resource>,
56 + highImagePreloads: Set<Resource>,
57 + // usedImagePreloads: Set<Resource>,
58 + styles: Map<string, StyleQueue>,
59 + bootstrapScripts: Set<Resource>,
60 + scripts: Set<Resource>,
61 + bulkPreloads: Set<Resource>,
62 + preloads: {
63 + images: Map<string, Resource>,
64 + stylesheets: Map<string, Resource>,
65 + scripts: Map<string, Resource>,
66 + moduleScripts: Map<string, Resource>,
67 + },
68 boundaryResources: ?BoundaryResources,
69 stylesToHoist: boolean,
70 // This is an extra field for the legacy renderer
@@ -94,12 +103,11 @@ export function createRenderState(
103 fontPreloads: renderState.fontPreloads,
104 highImagePreloads: renderState.highImagePreloads,
105 // usedImagePreloads: renderState.usedImagePreloads,
97 - precedences: renderState.precedences,
98 - stylePrecedences: renderState.stylePrecedences,
106 + styles: renderState.styles,
107 bootstrapScripts: renderState.bootstrapScripts,
108 scripts: renderState.scripts,
109 bulkPreloads: renderState.bulkPreloads,
102 - preloadsMap: renderState.preloadsMap,
110 + preloads: renderState.preloads,
111 boundaryResources: renderState.boundaryResources,
112 stylesToHoist: renderState.stylesToHoist,
113
packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js
+2 -2
@@ -84,7 +84,7 @@ describe('ReactDOMFizzServerBrowser', () => {
84 );
85 const result = await readResult(stream);
86 expect(result).toMatchInlineSnapshot(
87 - `"<link rel="preload" href="init.js" as="script" fetchPriority="low"/><link rel="modulepreload" href="init.mjs" fetchPriority="low"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
87 + `"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
88 );
89 });
90
@@ -505,7 +505,7 @@ describe('ReactDOMFizzServerBrowser', () => {
505 );
506 const result = await readResult(stream);
507 expect(result).toMatchInlineSnapshot(
508 - `"<link rel="preload" href="init.js" as="script" fetchPriority="low" nonce="R4nd0m"/><link rel="modulepreload" href="init.mjs" fetchPriority="low" nonce="R4nd0m"/><div>hello world</div><script nonce="${nonce}">INIT();</script><script src="init.js" nonce="${nonce}" async=""></script><script type="module" src="init.mjs" nonce="${nonce}" async=""></script>"`,
508 + `"<link rel="preload" as="script" fetchPriority="low" nonce="R4nd0m" href="init.js"/><link rel="modulepreload" fetchPriority="low" nonce="R4nd0m" href="init.mjs"/><div>hello world</div><script nonce="${nonce}">INIT();</script><script src="init.js" nonce="${nonce}" async=""></script><script type="module" src="init.mjs" nonce="${nonce}" async=""></script>"`,
509 );
510 });
511
packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js
+1 -1
@@ -98,7 +98,7 @@ describe('ReactDOMFizzServerNode', () => {
98 pipe(writable);
99 jest.runAllTimers();
100 expect(output.result).toMatchInlineSnapshot(
101 - `"<link rel="preload" href="init.js" as="script" fetchPriority="low"/><link rel="modulepreload" href="init.mjs" fetchPriority="low"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
101 + `"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
102 );
103 });
104
packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
+1 -1
@@ -145,7 +145,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
145 });
146 const prelude = await readContent(result.prelude);
147 expect(prelude).toMatchInlineSnapshot(
148 - `"<link rel="preload" href="init.js" as="script" fetchPriority="low"/><link rel="modulepreload" href="init.mjs" fetchPriority="low"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
148 + `"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
149 );
150 });
151
packages/react-dom/src/__tests__/ReactDOMFizzStaticFloat-test.js new
+275
@@ -0,0 +1,275 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +import {
13 + getVisibleChildren,
14 + insertNodesAndExecuteScripts,
15 +} from '../test-utils/FizzTestUtils';
16 +
17 +// Polyfills for test environment
18 +global.ReadableStream =
19 + require('web-streams-polyfill/ponyfill/es6').ReadableStream;
20 +global.TextEncoder = require('util').TextEncoder;
21 +
22 +let React;
23 +let ReactDOM;
24 +let ReactDOMFizzServer;
25 +let ReactDOMFizzStatic;
26 +let Suspense;
27 +let container;
28 +
29 +describe('ReactDOMFizzStaticFloat', () => {
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');
37 + }
38 + Suspense = React.Suspense;
39 + container = document.createElement('div');
40 + document.body.appendChild(container);
41 + });
42 +
43 + afterEach(() => {
44 + document.body.removeChild(container);
45 + });
46 +
47 + async function readIntoContainer(stream) {
48 + const reader = stream.getReader();
49 + let result = '';
50 + while (true) {
51 + const {done, value} = await reader.read();
52 + if (done) {
53 + break;
54 + }
55 + result += Buffer.from(value).toString('utf8');
56 + }
57 + const temp = document.createElement('div');
58 + temp.innerHTML = result;
59 + await insertNodesAndExecuteScripts(temp, container, null);
60 + }
61 +
62 + // @gate enablePostpone
63 + it('should transfer connection credentials across prerender and resume for stylesheets, scripts, and moduleScripts', async () => {
64 + let prerendering = true;
65 + function Postpone() {
66 + if (prerendering) {
67 + React.unstable_postpone();
68 + }
69 + return (
70 + <>
71 + <link rel="stylesheet" href="style creds" precedence="default" />
72 + <script async={true} src="script creds" data-meaningful="" />
73 + <script
74 + type="module"
75 + async={true}
76 + src="module creds"
77 + data-meaningful=""
78 + />
79 + <link rel="stylesheet" href="style anon" precedence="default" />
80 + <script async={true} src="script anon" data-meaningful="" />
81 + <script
82 + type="module"
83 + async={true}
84 + src="module default"
85 + data-meaningful=""
86 + />
87 + </>
88 + );
89 + }
90 +
91 + function App() {
92 + ReactDOM.preload('style creds', {
93 + as: 'style',
94 + crossOrigin: 'use-credentials',
95 + });
96 + ReactDOM.preload('script creds', {
97 + as: 'script',
98 + crossOrigin: 'use-credentials',
99 + integrity: 'script-hash',
100 + });
101 + ReactDOM.preloadModule('module creds', {
102 + crossOrigin: 'use-credentials',
103 + integrity: 'module-hash',
104 + });
105 + ReactDOM.preload('style anon', {
106 + as: 'style',
107 + crossOrigin: 'anonymous',
108 + });
109 + ReactDOM.preload('script anon', {
110 + as: 'script',
111 + crossOrigin: 'foobar',
112 + });
113 + ReactDOM.preloadModule('module default', {
114 + integrity: 'module-hash',
115 + });
116 + return (
117 + <div>
118 + <Suspense fallback="Loading...">
119 + <Postpone />
120 + </Suspense>
121 + </div>
122 + );
123 + }
124 +
125 + jest.mock('script creds', () => {}, {
126 + virtual: true,
127 + });
128 + jest.mock('module creds', () => {}, {
129 + virtual: true,
130 + });
131 + jest.mock('script anon', () => {}, {
132 + virtual: true,
133 + });
134 + jest.mock('module default', () => {}, {
135 + virtual: true,
136 + });
137 +
138 + const prerendered = await ReactDOMFizzStatic.prerender(<App />);
139 + expect(prerendered.postponed).not.toBe(null);
140 +
141 + await readIntoContainer(prerendered.prelude);
142 +
143 + expect(getVisibleChildren(container)).toEqual([
144 + <link
145 + rel="preload"
146 + as="style"
147 + href="style creds"
148 + crossorigin="use-credentials"
149 + />,
150 + <link
151 + rel="preload"
152 + as="script"
153 + href="script creds"
154 + crossorigin="use-credentials"
155 + integrity="script-hash"
156 + />,
157 + <link
158 + rel="modulepreload"
159 + href="module creds"
160 + crossorigin="use-credentials"
161 + integrity="module-hash"
162 + />,
163 + <link rel="preload" as="style" href="style anon" crossorigin="" />,
164 + <link rel="preload" as="script" href="script anon" crossorigin="" />,
165 + <link
166 + rel="modulepreload"
167 + href="module default"
168 + integrity="module-hash"
169 + />,
170 + <div>Loading...</div>,
171 + ]);
172 +
173 + prerendering = false;
174 + const content = await ReactDOMFizzServer.resume(
175 + <App />,
176 + JSON.parse(JSON.stringify(prerendered.postponed)),
177 + );
178 +
179 + await readIntoContainer(content);
180 +
181 + // Dispatch load event to injected stylesheet
182 + const linkCreds = document.querySelector(
183 + 'link[rel="stylesheet"][href="style creds"]',
184 + );
185 + const linkAnon = document.querySelector(
186 + 'link[rel="stylesheet"][href="style anon"]',
187 + );
188 + const event = document.createEvent('Events');
189 + event.initEvent('load', true, true);
190 + linkCreds.dispatchEvent(event);
191 + linkAnon.dispatchEvent(event);
192 +
193 + // Wait for the instruction microtasks to flush.
194 + await 0;
195 + await 0;
196 +
197 + expect(getVisibleChildren(document)).toEqual(
198 + <html>
199 + <head>
200 + <link
201 + rel="stylesheet"
202 + data-precedence="default"
203 + href="style creds"
204 + crossorigin="use-credentials"
205 + />
206 + <link
207 + rel="stylesheet"
208 + data-precedence="default"
209 + href="style anon"
210 + crossorigin=""
211 + />
212 + </head>
213 + <body>
214 + <div>
215 + <link
216 + rel="preload"
217 + as="style"
218 + href="style creds"
219 + crossorigin="use-credentials"
220 + />
221 + <link
222 + rel="preload"
223 + as="script"
224 + href="script creds"
225 + crossorigin="use-credentials"
226 + integrity="script-hash"
227 + />
228 + <link
229 + rel="modulepreload"
230 + href="module creds"
231 + crossorigin="use-credentials"
232 + integrity="module-hash"
233 + />
234 + <link rel="preload" as="style" href="style anon" crossorigin="" />
235 + <link rel="preload" as="script" href="script anon" crossorigin="" />
236 + <link
237 + rel="modulepreload"
238 + href="module default"
239 + integrity="module-hash"
240 + />
241 + <div />
242 + <script
243 + async=""
244 + src="script creds"
245 + crossorigin="use-credentials"
246 + integrity="script-hash"
247 + data-meaningful=""
248 + />
249 + <script
250 + type="module"
251 + async=""
252 + src="module creds"
253 + crossorigin="use-credentials"
254 + integrity="module-hash"
255 + data-meaningful=""
256 + />
257 + <script
258 + async=""
259 + src="script anon"
260 + crossorigin=""
261 + data-meaningful=""
262 + />
263 + <script
264 + type="module"
265 + async=""
266 + src="module default"
267 + integrity="module-hash"
268 + data-meaningful=""
269 + />
270 + </div>
271 + </body>
272 + </html>,
273 + );
274 + });
275 +});
packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js
+1 -1
@@ -86,7 +86,7 @@ describe('ReactDOMFizzStaticNode', () => {
86 );
87 const prelude = await readContent(result.prelude);
88 expect(prelude).toMatchInlineSnapshot(
89 - `"<link rel="preload" href="init.js" as="script" fetchPriority="low"/><link rel="modulepreload" href="init.mjs" fetchPriority="low"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
89 + `"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
90 );
91 });
92
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+576
@@ -4014,6 +4014,87 @@ body {
4014 );
4015 });
4016
4017 + it('can promote images to high priority when at least one instance specifies a high fetchPriority', async () => {
4018 + function App() {
4019 + // If a ends up in a higher priority queue than b it will flush first
4020 + ReactDOM.preload('a', {as: 'image'});
4021 + ReactDOM.preload('b', {as: 'image'});
4022 + return (
4023 + <html>
4024 + <body>
4025 + <link rel="stylesheet" href="foo" precedence="default" />
4026 + <img src="1" />
4027 + <img src="2" />
4028 + <img src="3" />
4029 + <img src="4" />
4030 + <img src="5" />
4031 + <img src="6" />
4032 + <img src="7" />
4033 + <img src="8" />
4034 + <img src="9" />
4035 + <img src="10" />
4036 + <img src="11" />
4037 + <img src="12" />
4038 + <img src="a" fetchPriority="low" />
4039 + <img src="a" />
4040 + <img src="a" fetchPriority="high" />
4041 + <img src="a" />
4042 + <img src="a" />
4043 + </body>
4044 + </html>
4045 + );
4046 + }
4047 +
4048 + await act(() => {
4049 + renderToPipeableStream(<App />).pipe(writable);
4050 + });
4051 + expect(getMeaningfulChildren(document)).toEqual(
4052 + <html>
4053 + <head>
4054 + {/* The First 10 high priority images were just the first 10 rendered images */}
4055 + <link rel="preload" as="image" href="1" />
4056 + <link rel="preload" as="image" href="2" />
4057 + <link rel="preload" as="image" href="3" />
4058 + <link rel="preload" as="image" href="4" />
4059 + <link rel="preload" as="image" href="5" />
4060 + <link rel="preload" as="image" href="6" />
4061 + <link rel="preload" as="image" href="7" />
4062 + <link rel="preload" as="image" href="8" />
4063 + <link rel="preload" as="image" href="9" />
4064 + <link rel="preload" as="image" href="10" />
4065 + {/* The "a" image was rendered a few times but since at least one of those was with
4066 + fetchPriorty="high" it ends up in the high priority queue */}
4067 + <link rel="preload" as="image" href="a" />
4068 + {/* Stylesheets come in between high priority images and regular preloads */}
4069 + <link rel="stylesheet" href="foo" data-precedence="default" />
4070 + {/* The remainig images that preloaded at regular priority */}
4071 + <link rel="preload" as="image" href="b" />
4072 + <link rel="preload" as="image" href="11" />
4073 + <link rel="preload" as="image" href="12" />
4074 + </head>
4075 + <body>
4076 + <img src="1" />
4077 + <img src="2" />
4078 + <img src="3" />
4079 + <img src="4" />
4080 + <img src="5" />
4081 + <img src="6" />
4082 + <img src="7" />
4083 + <img src="8" />
4084 + <img src="9" />
4085 + <img src="10" />
4086 + <img src="11" />
4087 + <img src="12" />
4088 + <img src="a" fetchpriority="low" />
4089 + <img src="a" />
4090 + <img src="a" fetchpriority="high" />
4091 + <img src="a" />
4092 + <img src="a" />
4093 + </body>
4094 + </html>,
4095 + );
4096 + });
4097 +
4098 it('preloads from rendered images properly use srcSet and sizes', async () => {
4099 function App() {
4100 ReactDOM.preload('1', {as: 'image', imageSrcSet: 'ss1'});
@@ -4119,6 +4200,501 @@ body {
4200 );
4201 });
4202
4203 + it('should warn if you preload a stylesheet and then render a style tag with the same href', async () => {
4204 + const style = 'body { color: red; }';
4205 + function App() {
4206 + ReactDOM.preload('foo', {as: 'style'});
4207 + return (
4208 + <html>
4209 + <body>
4210 + hello
4211 + <style precedence="default" href="foo">
4212 + {style}
4213 + </style>
4214 + </body>
4215 + </html>
4216 + );
4217 + }
4218 +
4219 + await expect(async () => {
4220 + await act(() => {
4221 + renderToPipeableStream(<App />).pipe(writable);
4222 + });
4223 + }).toErrorDev([
4224 + 'React encountered a hoistable style tag for the same href as a preload: "foo". When using a style tag to inline styles you should not also preload it as a stylsheet.',
4225 + ]);
4226 +
4227 + expect(getMeaningfulChildren(document)).toEqual(
4228 + <html>
4229 + <head>
4230 + <style data-precedence="default" data-href="foo">
4231 + {style}
4232 + </style>
4233 + <link rel="preload" as="style" href="foo" />
4234 + </head>
4235 + <body>hello</body>
4236 + </html>,
4237 + );
4238 + });
4239 +
4240 + it('should preload only once even if you discover a stylesheet, script, or moduleScript late', async () => {
4241 + function App() {
4242 + // We start with preinitializing some resources first
4243 + ReactDOM.preinit('shell preinit/shell', {as: 'style'});
4244 + ReactDOM.preinit('shell preinit/shell', {as: 'script'});
4245 + ReactDOM.preinitModule('shell preinit/shell', {as: 'script'});
4246 +
4247 + // We initiate all the shell preloads
4248 + ReactDOM.preload('shell preinit/shell', {as: 'style'});
4249 + ReactDOM.preload('shell preinit/shell', {as: 'script'});
4250 + ReactDOM.preloadModule('shell preinit/shell', {as: 'script'});
4251 +
4252 + ReactDOM.preload('shell/shell preinit', {as: 'style'});
4253 + ReactDOM.preload('shell/shell preinit', {as: 'script'});
4254 + ReactDOM.preloadModule('shell/shell preinit', {as: 'script'});
4255 +
4256 + ReactDOM.preload('shell/shell render', {as: 'style'});
4257 + ReactDOM.preload('shell/shell render', {as: 'script'});
4258 + ReactDOM.preloadModule('shell/shell render');
4259 +
4260 + ReactDOM.preload('shell/late preinit', {as: 'style'});
4261 + ReactDOM.preload('shell/late preinit', {as: 'script'});
4262 + ReactDOM.preloadModule('shell/late preinit');
4263 +
4264 + ReactDOM.preload('shell/late render', {as: 'style'});
4265 + ReactDOM.preload('shell/late render', {as: 'script'});
4266 + ReactDOM.preloadModule('shell/late render');
4267 +
4268 + // we preinit later ones that should be created by
4269 + ReactDOM.preinit('shell/shell preinit', {as: 'style'});
4270 + ReactDOM.preinit('shell/shell preinit', {as: 'script'});
4271 + ReactDOM.preinitModule('shell/shell preinit');
4272 +
4273 + ReactDOM.preinit('late/shell preinit', {as: 'style'});
4274 + ReactDOM.preinit('late/shell preinit', {as: 'script'});
4275 + ReactDOM.preinitModule('late/shell preinit');
4276 + return (
4277 + <html>
4278 + <body>
4279 + <link
4280 + rel="stylesheet"
4281 + precedence="default"
4282 + href="shell/shell render"
4283 + />
4284 + <script async={true} src="shell/shell render" />
4285 + <script type="module" async={true} src="shell/shell render" />
4286 + <link
4287 + rel="stylesheet"
4288 + precedence="default"
4289 + href="late/shell render"
4290 + />
4291 + <script async={true} src="late/shell render" />
4292 + <script type="module" async={true} src="late/shell render" />
4293 + <Suspense fallback="late...">
4294 + <BlockedOn value="late">
4295 + <Late />
4296 + </BlockedOn>
4297 + </Suspense>
4298 + <Suspense fallback="later...">
4299 + <BlockedOn value="later">
4300 + <Later />
4301 + </BlockedOn>
4302 + </Suspense>
4303 + </body>
4304 + </html>
4305 + );
4306 + }
4307 +
4308 + function Late() {
4309 + ReactDOM.preload('late/later preinit', {as: 'style'});
4310 + ReactDOM.preload('late/later preinit', {as: 'script'});
4311 + ReactDOM.preloadModule('late/later preinit');
4312 +
4313 + ReactDOM.preload('late/later render', {as: 'style'});
4314 + ReactDOM.preload('late/later render', {as: 'script'});
4315 + ReactDOM.preloadModule('late/later render');
4316 +
4317 + ReactDOM.preload('late/shell preinit', {as: 'style'});
4318 + ReactDOM.preload('late/shell preinit', {as: 'script'});
4319 + ReactDOM.preloadModule('late/shell preinit');
4320 +
4321 + ReactDOM.preload('late/shell render', {as: 'style'});
4322 + ReactDOM.preload('late/shell render', {as: 'script'});
4323 + ReactDOM.preloadModule('late/shell render');
4324 +
4325 + // late preinits don't actually flush so we won't see this in the DOM as a stylesehet but we should see
4326 + // the preload for this resource
4327 + ReactDOM.preinit('shell/late preinit', {as: 'style'});
4328 + ReactDOM.preinit('shell/late preinit', {as: 'script'});
4329 + ReactDOM.preinitModule('shell/late preinit');
4330 + return (
4331 + <>
4332 + Late
4333 + <link
4334 + rel="stylesheet"
4335 + precedence="default"
4336 + href="shell/late render"
4337 + />
4338 + <script async={true} src="shell/late render" />
4339 + <script type="module" async={true} src="shell/late render" />
4340 + </>
4341 + );
4342 + }
4343 +
4344 + function Later() {
4345 + // late preinits don't actually flush so we won't see this in the DOM as a stylesehet but we should see
4346 + // the preload for this resource
4347 + ReactDOM.preinit('late/later preinit', {as: 'style'});
4348 + ReactDOM.preinit('late/later preinit', {as: 'script'});
4349 + ReactDOM.preinitModule('late/later preinit');
4350 + return (
4351 + <>
4352 + Later
4353 + <link
4354 + rel="stylesheet"
4355 + precedence="default"
4356 + href="late/later render"
4357 + />
4358 + <script async={true} src="late/later render" />
4359 + <script type="module" async={true} src="late/later render" />
4360 + </>
4361 + );
4362 + }
4363 +
4364 + await act(() => {
4365 + renderToPipeableStream(<App />).pipe(writable);
4366 + });
4367 + expect(getMeaningfulChildren(document)).toEqual(
4368 + <html>
4369 + <head>
4370 + <link
4371 + rel="stylesheet"
4372 + data-precedence="default"
4373 + href="shell preinit/shell"
4374 + />
4375 + <link
4376 + rel="stylesheet"
4377 + data-precedence="default"
4378 + href="shell/shell preinit"
4379 + />
4380 + <link
4381 + rel="stylesheet"
4382 + data-precedence="default"
4383 + href="late/shell preinit"
4384 + />
4385 + <link
4386 + rel="stylesheet"
4387 + data-precedence="default"
4388 + href="shell/shell render"
4389 + />
4390 + <link
4391 + rel="stylesheet"
4392 + data-precedence="default"
4393 + href="late/shell render"
4394 + />
4395 + <script async="" src="shell preinit/shell" />
4396 + <script async="" src="shell preinit/shell" type="module" />
4397 + <script async="" src="shell/shell preinit" />
4398 + <script async="" src="shell/shell preinit" type="module" />
4399 + <script async="" src="late/shell preinit" />
4400 + <script async="" src="late/shell preinit" type="module" />
4401 + <script async="" src="shell/shell render" />
4402 + <script async="" src="shell/shell render" type="module" />
4403 + <script async="" src="late/shell render" />
4404 + <script async="" src="late/shell render" type="module" />
4405 + <link rel="preload" as="style" href="shell/late preinit" />
4406 + <link rel="preload" as="script" href="shell/late preinit" />
4407 + <link rel="modulepreload" href="shell/late preinit" />
4408 + <link rel="preload" as="style" href="shell/late render" />
4409 + <link rel="preload" as="script" href="shell/late render" />
4410 + <link rel="modulepreload" href="shell/late render" />
4411 + </head>
4412 + <body>
4413 + {'late...'}
4414 + {'later...'}
4415 + </body>
4416 + </html>,
4417 + );
4418 +
4419 + await act(() => {
4420 + resolveText('late');
4421 + });
4422 + expect(getMeaningfulChildren(document)).toEqual(
4423 + <html>
4424 + <head>
4425 + <link
4426 + rel="stylesheet"
4427 + data-precedence="default"
4428 + href="shell preinit/shell"
4429 + />
4430 + <link
4431 + rel="stylesheet"
4432 + data-precedence="default"
4433 + href="shell/shell preinit"
4434 + />
4435 + <link
4436 + rel="stylesheet"
4437 + data-precedence="default"
4438 + href="late/shell preinit"
4439 + />
4440 + <link
4441 + rel="stylesheet"
4442 + data-precedence="default"
4443 + href="shell/shell render"
4444 + />
4445 + <link
4446 + rel="stylesheet"
4447 + data-precedence="default"
4448 + href="late/shell render"
4449 + />
4450 + {/* FROM HERE */}
4451 + <link
4452 + rel="stylesheet"
4453 + data-precedence="default"
4454 + href="shell/late render"
4455 + />
4456 + {/** TO HERE:
4457 + * This was hoisted by boundary complete instruction. The preload was already emitted in the
4458 + * shell but we see it below because this was inserted clientside by precedence.
4459 + * We don't observe the "shell/late preinit" because these do not flush unless they are flushing
4460 + * with the shell
4461 + * */}
4462 + <script async="" src="shell preinit/shell" />
4463 + <script async="" src="shell preinit/shell" type="module" />
4464 + <script async="" src="shell/shell preinit" />
4465 + <script async="" src="shell/shell preinit" type="module" />
4466 + <script async="" src="late/shell preinit" />
4467 + <script async="" src="late/shell preinit" type="module" />
4468 + <script async="" src="shell/shell render" />
4469 + <script async="" src="shell/shell render" type="module" />
4470 + <script async="" src="late/shell render" />
4471 + <script async="" src="late/shell render" type="module" />
4472 + <link rel="preload" as="style" href="shell/late preinit" />
4473 + <link rel="preload" as="script" href="shell/late preinit" />
4474 + <link rel="modulepreload" href="shell/late preinit" />
4475 + <link rel="preload" as="style" href="shell/late render" />
4476 + <link rel="preload" as="script" href="shell/late render" />
4477 + <link rel="modulepreload" href="shell/late render" />
4478 + </head>
4479 + <body>
4480 + {'late...'}
4481 + {'later...'}
4482 + {/* FROM HERE */}
4483 + <script async="" src="shell/late preinit" />
4484 + <script async="" src="shell/late preinit" type="module" />
4485 + <script async="" src="shell/late render" />
4486 + <script async="" src="shell/late render" type="module" />
4487 + <link rel="preload" as="style" href="late/later preinit" />
4488 + <link rel="preload" as="script" href="late/later preinit" />
4489 + <link rel="modulepreload" href="late/later preinit" />
4490 + <link rel="preload" as="style" href="late/later render" />
4491 + <link rel="preload" as="script" href="late/later render" />
4492 + <link rel="modulepreload" href="late/later render" />
4493 + {/** TO HERE:
4494 + * These resources streamed into the body during the boundary flush. Scripts go first then
4495 + * preloads according to our streaming queue priorities. Note also that late/shell resources
4496 + * where the resource already emitted in the shell and the preload is invoked later do not
4497 + * end up with a preload in the document at all.
4498 + * */}
4499 + </body>
4500 + </html>,
4501 + );
4502 +
4503 + await act(() => {
4504 + resolveText('later');
4505 + });
4506 + expect(getMeaningfulChildren(document)).toEqual(
4507 + <html>
4508 + <head>
4509 + <link
4510 + rel="stylesheet"
4511 + data-precedence="default"
4512 + href="shell preinit/shell"
4513 + />
4514 + <link
4515 + rel="stylesheet"
4516 + data-precedence="default"
4517 + href="shell/shell preinit"
4518 + />
4519 + <link
4520 + rel="stylesheet"
4521 + data-precedence="default"
4522 + href="late/shell preinit"
4523 + />
4524 + <link
4525 + rel="stylesheet"
4526 + data-precedence="default"
4527 + href="shell/shell render"
4528 + />
4529 + <link
4530 + rel="stylesheet"
4531 + data-precedence="default"
4532 + href="late/shell render"
4533 + />
4534 + <link
4535 + rel="stylesheet"
4536 + data-precedence="default"
4537 + href="shell/late render"
4538 + />
4539 + {/* FROM HERE */}
4540 + <link
4541 + rel="stylesheet"
4542 + data-precedence="default"
4543 + href="late/later render"
4544 + />
4545 + {/** TO HERE:
4546 + * This was hoisted by boundary complete instruction. The preload was already emitted in the
4547 + * shell but we see it below because this was inserted clientside by precedence
4548 + * We don't observe the "late/later preinit" because these do not flush unless they are flushing
4549 + * with the shell
4550 + * */}
4551 + <script async="" src="shell preinit/shell" />
4552 + <script async="" src="shell preinit/shell" type="module" />
4553 + <script async="" src="shell/shell preinit" />
4554 + <script async="" src="shell/shell preinit" type="module" />
4555 + <script async="" src="late/shell preinit" />
4556 + <script async="" src="late/shell preinit" type="module" />
4557 + <script async="" src="shell/shell render" />
4558 + <script async="" src="shell/shell render" type="module" />
4559 + <script async="" src="late/shell render" />
4560 + <script async="" src="late/shell render" type="module" />
4561 + <link rel="preload" as="style" href="shell/late preinit" />
4562 + <link rel="preload" as="script" href="shell/late preinit" />
4563 + <link rel="modulepreload" href="shell/late preinit" />
4564 + <link rel="preload" as="style" href="shell/late render" />
4565 + <link rel="preload" as="script" href="shell/late render" />
4566 + <link rel="modulepreload" href="shell/late render" />
4567 + </head>
4568 + <body>
4569 + {'late...'}
4570 + {'later...'}
4571 + <script async="" src="shell/late preinit" />
4572 + <script async="" src="shell/late preinit" type="module" />
4573 + <script async="" src="shell/late render" />
4574 + <script async="" src="shell/late render" type="module" />
4575 + <link rel="preload" as="style" href="late/later preinit" />
4576 + <link rel="preload" as="script" href="late/later preinit" />
4577 + <link rel="modulepreload" href="late/later preinit" />
4578 + <link rel="preload" as="style" href="late/later render" />
4579 + <link rel="preload" as="script" href="late/later render" />
4580 + <link rel="modulepreload" href="late/later render" />
4581 + {/* FROM HERE */}
4582 + <script async="" src="late/later preinit" />
4583 + <script async="" src="late/later preinit" type="module" />
4584 + <script async="" src="late/later render" />
4585 + <script async="" src="late/later render" type="module" />
4586 + {/** TO HERE:
4587 + * These resources streamed into the body during the boundary flush. Scripts go first then
4588 + * preloads according to our streaming queue priorities
4589 + * */}
4590 + </body>
4591 + </html>,
4592 + );
4593 + loadStylesheets();
4594 + assertLog([
4595 + 'load stylesheet: shell preinit/shell',
4596 + 'load stylesheet: shell/shell preinit',
4597 + 'load stylesheet: late/shell preinit',
4598 + 'load stylesheet: shell/shell render',
4599 + 'load stylesheet: late/shell render',
4600 + 'load stylesheet: shell/late render',
4601 + 'load stylesheet: late/later render',
4602 + ]);
4603 +
4604 + ReactDOMClient.hydrateRoot(document, <App />);
4605 + await waitForAll([]);
4606 + expect(getMeaningfulChildren(document)).toEqual(
4607 + <html>
4608 + <head>
4609 + <link
4610 + rel="stylesheet"
4611 + data-precedence="default"
4612 + href="shell preinit/shell"
4613 + />
4614 + <link
4615 + rel="stylesheet"
4616 + data-precedence="default"
4617 + href="shell/shell preinit"
4618 + />
4619 + <link
4620 + rel="stylesheet"
4621 + data-precedence="default"
4622 + href="late/shell preinit"
4623 + />
4624 + <link
4625 + rel="stylesheet"
4626 + data-precedence="default"
4627 + href="shell/shell render"
4628 + />
4629 + <link
4630 + rel="stylesheet"
4631 + data-precedence="default"
4632 + href="late/shell render"
4633 + />
4634 + <link
4635 + rel="stylesheet"
4636 + data-precedence="default"
4637 + href="shell/late render"
4638 + />
4639 + <link
4640 + rel="stylesheet"
4641 + data-precedence="default"
4642 + href="late/later render"
4643 + />
4644 + {/* FROM HERE */}
4645 + <link
4646 + rel="stylesheet"
4647 + data-precedence="default"
4648 + href="shell/late preinit"
4649 + />
4650 + <link
4651 + rel="stylesheet"
4652 + data-precedence="default"
4653 + href="late/later preinit"
4654 + />
4655 + {/** TO HERE:
4656 + * The client render patches in the two missing preinit stylesheets when hydration happens
4657 + * Note that this is only because we repeated the calls to preinit on the client
4658 + * */}
4659 + <script async="" src="shell preinit/shell" />
4660 + <script async="" src="shell preinit/shell" type="module" />
4661 + <script async="" src="shell/shell preinit" />
4662 + <script async="" src="shell/shell preinit" type="module" />
4663 + <script async="" src="late/shell preinit" />
4664 + <script async="" src="late/shell preinit" type="module" />
4665 + <script async="" src="shell/shell render" />
4666 + <script async="" src="shell/shell render" type="module" />
4667 + <script async="" src="late/shell render" />
4668 + <script async="" src="late/shell render" type="module" />
4669 + <link rel="preload" as="style" href="shell/late preinit" />
4670 + <link rel="preload" as="script" href="shell/late preinit" />
4671 + <link rel="modulepreload" href="shell/late preinit" />
4672 + <link rel="preload" as="style" href="shell/late render" />
4673 + <link rel="preload" as="script" href="shell/late render" />
4674 + <link rel="modulepreload" href="shell/late render" />
4675 + </head>
4676 + <body>
4677 + {'Late'}
4678 + {'Later'}
4679 + <script async="" src="shell/late preinit" />
4680 + <script async="" src="shell/late preinit" type="module" />
4681 + <script async="" src="shell/late render" />
4682 + <script async="" src="shell/late render" type="module" />
4683 + <link rel="preload" as="style" href="late/later preinit" />
4684 + <link rel="preload" as="script" href="late/later preinit" />
4685 + <link rel="modulepreload" href="late/later preinit" />
4686 + <link rel="preload" as="style" href="late/later render" />
4687 + <link rel="preload" as="script" href="late/later render" />
4688 + <link rel="modulepreload" href="late/later render" />
4689 + <script async="" src="late/later preinit" />
4690 + <script async="" src="late/later preinit" type="module" />
4691 + <script async="" src="late/later render" />
4692 + <script async="" src="late/later render" type="module" />
4693 + </body>
4694 + </html>,
4695 + );
4696 + });
4697 +
4698 describe('ReactDOM.prefetchDNS(href)', () => {
4699 it('creates a dns-prefetch resource when called', async () => {
4700 function App({url}) {
packages/react-dom/src/shared/ReactDOMTypes.js
+5 -1
@@ -24,6 +24,7 @@ export type PreloadModuleOptions = {
24 as?: string,
25 crossOrigin?: string,
26 integrity?: string,
27 + nonce?: string,
28 };
29 export type PreinitOptions = {
30 as: string,
@@ -37,6 +38,7 @@ export type PreinitModuleOptions = {
38 as?: string,
39 crossOrigin?: string,
40 integrity?: string,
41 + nonce?: string,
42 };
43
44 export type CrossOriginEnum = '' | 'use-credentials';
@@ -56,6 +58,7 @@ export type PreloadModuleImplOptions = {
58 as?: ?string,
59 crossOrigin?: ?CrossOriginEnum,
60 integrity?: ?string,
61 + nonce?: ?string,
62 };
63 export type PreinitStyleOptions = {
64 crossOrigin?: ?string,
@@ -70,7 +73,8 @@ export type PreinitScriptOptions = {
73 };
74 export type PreinitModuleScriptOptions = {
75 crossOrigin?: ?CrossOriginEnum,
73 - integrity?: string,
76 + integrity?: ?string,
77 + nonce?: ?string,
78 };
79
80 export type HostDispatcher = {
packages/react-dom/src/test-utils/FizzTestUtils.js
+3 -2
@@ -93,6 +93,7 @@ async function executeScript(script: Element) {
93 'You must set the current document to the global document to use script src in tests',
94 );
95 }
96 +
97 try {
98 // $FlowFixMe
99 require(scriptSrc);
@@ -177,8 +178,8 @@ function getVisibleChildren(element: Element): React$Node {
178 while (node) {
179 if (node.nodeType === 1) {
180 if (
180 - node.tagName !== 'SCRIPT' &&
181 - node.tagName !== 'script' &&
181 + ((node.tagName !== 'SCRIPT' && node.tagName !== 'script') ||
182 + node.hasAttribute('data-meaningful')) &&
183 node.tagName !== 'TEMPLATE' &&
184 node.tagName !== 'template' &&
185 !node.hasAttribute('hidden') &&
packages/react-server-dom-fb/src/__tests__/ReactDOMServerFB-test.internal.js
+1 -1
@@ -59,7 +59,7 @@ describe('ReactDOMServerFB', () => {
59 });
60 const result = readResult(stream);
61 expect(result).toMatchInlineSnapshot(
62 - `"<link rel="preload" href="init.js" as="script" fetchPriority="low"/><link rel="modulepreload" href="init.mjs" fetchPriority="low"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
62 + `"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
63 );
64 });
65