From 49eba01930e9e1f331b34967fca65d5a0ba62846 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Tue, 26 Sep 2023 09:59:39 -0700 Subject: [PATCH] [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. --- .../src/server/ReactFizzConfigDOM.js | 1489 +++++++++-------- .../src/server/ReactFizzConfigDOMLegacy.js | 36 +- .../ReactDOMFizzServerBrowser-test.js | 4 +- .../__tests__/ReactDOMFizzServerNode-test.js | 2 +- .../ReactDOMFizzStaticBrowser-test.js | 2 +- .../__tests__/ReactDOMFizzStaticFloat-test.js | 275 +++ .../__tests__/ReactDOMFizzStaticNode-test.js | 2 +- .../src/__tests__/ReactDOMFloat-test.js | 576 +++++++ .../react-dom/src/shared/ReactDOMTypes.js | 6 +- .../react-dom/src/test-utils/FizzTestUtils.js | 5 +- .../ReactDOMServerFB-test.internal.js | 2 +- 11 files changed, 1710 insertions(+), 689 deletions(-) create mode 100644 packages/react-dom/src/__tests__/ReactDOMFizzStaticFloat-test.js diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 5dd228795ca87..9c82cb6579231 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -148,18 +148,22 @@ export type RenderState = { hoistableChunks: Array, // Flushing queues for Resource dependencies - preconnects: Set, - fontPreloads: Set, - highImagePreloads: Set, + preconnects: Set, + fontPreloads: Set, + highImagePreloads: Set, // usedImagePreloads: Set, - precedences: Map>, - stylePrecedences: Map, - bootstrapScripts: Set, - scripts: Set, - bulkPreloads: Set, + styles: Map, + bootstrapScripts: Set, + scripts: Set, + bulkPreloads: Set, // Temporarily keeps track of key to preload resources before shell flushes. - preloadsMap: Map, + preloads: { + images: Map, + stylesheets: Map, + scripts: Map, + moduleScripts: Map, + }, // Module-global-like reference for current boundary resources boundaryResources: ?BoundaryResources, @@ -174,6 +178,30 @@ export type RenderState = { ... }; +type Exists = null; +type Preloaded = []; +// Credentials here are things that affect whether a browser will make a request +// as well as things that affect which connection the browser will use for that request. +// We want these to be aligned across preloads and resources because otherwise the preload +// will be wasted. +// We investigated whether referrerPolicy should be included here but from experimentation +// it seems that browsers do not treat this as part of the http cache key and does not affect +// which connection is used. +type PreloadedWithCredentials = [ + /* crossOrigin */ ?string, + /* integrity */ ?string, +]; + +const EXISTS: Exists = null; +// This constant is to mark preloads that have no unique credentials +// to convey. It should never be checked by identity and we should not +// assume Preload values in ResumableState equal this value because they +// will have come from some parsed input. +const PRELOAD_NO_CREDS: Preloaded = []; +if (__DEV__) { + Object.freeze(PRELOAD_NO_CREDS); +} + // Per response, global state that is not contextual to the rendering subtree. // This is resumable and therefore should be serializable. export type ResumableState = { @@ -189,10 +217,34 @@ export type ResumableState = { hasHtml: boolean, // Resources - Request local cache - preloadsMap: {[key: string]: PreloadProps}, - preconnectsMap: {[key: string]: null}, - stylesMap: {[key: string]: null}, - scriptsMap: {[key: string]: null}, + unknownResources: { + [asType: string]: { + [href: string]: Preloaded, + }, + }, + dnsResources: {[key: string]: Exists}, + connectResources: { + default: {[key: string]: Exists}, + anonymous: {[key: string]: Exists}, + credentials: {[key: string]: Exists}, + }, + imageResources: { + [key: string]: Preloaded, + }, + styleResources: { + [key: string]: Exists | Preloaded | PreloadedWithCredentials, + }, + scriptResources: { + [key: string]: Exists | Preloaded | PreloadedWithCredentials, + }, + moduleUnknownResources: { + [asType: string]: { + [href: string]: Preloaded, + }, + }, + moduleScriptResources: { + [key: string]: Exists | Preloaded | PreloadedWithCredentials, + }, }; const dataElementQuotedEnd = stringToPrecomputedChunk('">'); @@ -342,13 +394,17 @@ export function createRenderState( fontPreloads: new Set(), highImagePreloads: new Set(), // usedImagePreloads: new Set(), - precedences: new Map(), - stylePrecedences: new Map(), + styles: new Map(), bootstrapScripts: new Set(), scripts: new Set(), bulkPreloads: new Set(), - preloadsMap: new Map(), + preloads: { + images: new Map(), + stylesheets: new Map(), + scripts: new Map(), + moduleScripts: new Map(), + }, nonce, // like a module global for currently rendering boundary @@ -359,25 +415,30 @@ export function createRenderState( if (bootstrapScripts !== undefined) { for (let i = 0; i < bootstrapScripts.length; i++) { const scriptConfig = bootstrapScripts[i]; - const src = - typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src; - const integrity = - typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity; - const crossOrigin = - typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null - ? undefined - : scriptConfig.crossOrigin === 'use-credentials' - ? 'use-credentials' - : ''; - - preloadBootstrapScript( - resumableState, - renderState, - src, + let src, crossOrigin, integrity; + const props: PreloadAsProps = ({ + rel: 'preload', + as: 'script', + fetchPriority: 'low', nonce, - integrity, - crossOrigin, - ); + }: any); + if (typeof scriptConfig === 'string') { + props.href = src = scriptConfig; + } else { + props.href = src = scriptConfig.src; + props.integrity = integrity = + typeof scriptConfig.integrity === 'string' + ? scriptConfig.integrity + : undefined; + props.crossOrigin = crossOrigin = + typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null + ? undefined + : scriptConfig.crossOrigin === 'use-credentials' + ? 'use-credentials' + : ''; + } + + preloadBootstrapScriptOrModule(resumableState, renderState, src, props); bootstrapChunks.push( startScriptSrc, @@ -389,7 +450,7 @@ export function createRenderState( stringToChunk(escapeTextForBrowser(nonce)), ); } - if (integrity) { + if (typeof integrity === 'string') { bootstrapChunks.push( scriptIntegirty, stringToChunk(escapeTextForBrowser(integrity)), @@ -407,25 +468,29 @@ export function createRenderState( if (bootstrapModules !== undefined) { for (let i = 0; i < bootstrapModules.length; i++) { const scriptConfig = bootstrapModules[i]; - const src = - typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src; - const integrity = - typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity; - const crossOrigin = - typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null - ? undefined - : scriptConfig.crossOrigin === 'use-credentials' - ? 'use-credentials' - : ''; - - preloadBootstrapModule( - resumableState, - renderState, - src, + let src, crossOrigin, integrity; + const props: PreloadModuleProps = ({ + rel: 'modulepreload', + fetchPriority: 'low', nonce, - integrity, - crossOrigin, - ); + }: any); + if (typeof scriptConfig === 'string') { + props.href = src = scriptConfig; + } else { + props.href = src = scriptConfig.src; + props.integrity = integrity = + typeof scriptConfig.integrity === 'string' + ? scriptConfig.integrity + : undefined; + props.crossOrigin = crossOrigin = + typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null + ? undefined + : scriptConfig.crossOrigin === 'use-credentials' + ? 'use-credentials' + : ''; + } + + preloadBootstrapScriptOrModule(resumableState, renderState, src, props); bootstrapChunks.push( startModuleSrc, @@ -438,7 +503,7 @@ export function createRenderState( stringToChunk(escapeTextForBrowser(nonce)), ); } - if (integrity) { + if (typeof integrity === 'string') { bootstrapChunks.push( scriptIntegirty, stringToChunk(escapeTextForBrowser(integrity)), @@ -496,10 +561,18 @@ export function createResumableState( // @TODO add bootstrap script to implicit preloads // persistent - preloadsMap: {}, - preconnectsMap: {}, - stylesMap: {}, - scriptsMap: {}, + unknownResources: {}, + dnsResources: {}, + connectResources: { + default: {}, + anonymous: {}, + credentials: {}, + }, + imageResources: {}, + styleResources: {}, + scriptResources: {}, + moduleUnknownResources: {}, + moduleScriptResources: {}, }; } @@ -2102,7 +2175,7 @@ function pushLink( if (props.rel === 'stylesheet') { // This may hoistable as a Stylesheet Resource, otherwise it will emit in place - const key = getResourceKey('style', href); + const key = getResourceKey(href); if ( typeof precedence !== 'string' || props.disabled != null || @@ -2136,60 +2209,62 @@ function pushLink( return pushLinkImpl(target, props); } else { // This stylesheet refers to a Resource and we create a new one if necessary - let stylesInPrecedence = renderState.precedences.get(precedence); - if (!resumableState.stylesMap.hasOwnProperty(key)) { - const resourceProps = stylesheetPropsFromRawProps(props); - let state = NoState; - if (resumableState.preloadsMap.hasOwnProperty(key)) { - const preloadProps: PreloadProps = resumableState.preloadsMap[key]; - adoptPreloadPropsForStylesheetProps(resourceProps, preloadProps); - const preloadResource = renderState.preloadsMap.get(key); - if (preloadResource) { - // If we already had a preload we don't want that resource to flush directly. - // We let the newly created resource govern flushing. - preloadResource.state |= Blocked; - if (preloadResource.state & Flushed) { - state = PreloadFlushed; - } - } else { - // If we resumed then we assume that this was already flushed - // by the shell. - state = PreloadFlushed; - } + let styleQueue = renderState.styles.get(precedence); + const hasKey = resumableState.styleResources.hasOwnProperty(key); + const resourceState = hasKey + ? resumableState.styleResources[key] + : undefined; + if (resourceState !== EXISTS) { + // We are going to create this resource now so it is marked as Exists + resumableState.styleResources[key] = EXISTS; + + // If this is the first time we've encountered this precedence we need + // to create a StyleQueue + if (!styleQueue) { + styleQueue = { + precedence: stringToChunk(escapeTextForBrowser(precedence)), + rules: ([]: Array), + hrefs: ([]: Array), + sheets: (new Map(): Map), + }; + renderState.styles.set(precedence, styleQueue); } - const resource = { - type: 'stylesheet', - chunks: ([]: Array), - state, - props: resourceProps, + + const resource: StylesheetResource = { + state: PENDING, + props: stylesheetPropsFromRawProps(props), }; - resumableState.stylesMap[key] = null; - if (!stylesInPrecedence) { - stylesInPrecedence = new Map(); - renderState.precedences.set(precedence, stylesInPrecedence); - const emptyStyleResource = { - type: 'style', - chunks: ([]: Array), - state: NoState, - props: { - precedence, - hrefs: ([]: Array), - }, - }; - stylesInPrecedence.set('', emptyStyleResource); - if (__DEV__) { - if (renderState.stylePrecedences.has(precedence)) { - console.error( - 'React constructed an empty style resource when a style resource already exists for this precedence: "%s". This is a bug in React.', - precedence, - ); - } + + if (resourceState) { + // When resourceState is truty it is a Preload state. We cast it for clarity + const preloadState: Preloaded | PreloadedWithCredentials = + resourceState; + if (preloadState.length === 2) { + adoptPreloadCredentials(resource.props, preloadState); + } + + const preloadResource = renderState.preloads.stylesheets.get(key); + if (preloadResource && preloadResource.length > 0) { + // The Preload for this resource was created in this render pass and has not flushed yet so + // we need to clear it to avoid it flushing. + preloadResource.length = 0; + } else { + // Either the preload resource from this render already flushed in this render pass + // or the preload flushed in a prior pass (prerender). In either case we need to mark + // this resource as already having been preloaded. + resource.state = PRELOADED; } - renderState.stylePrecedences.set(precedence, emptyStyleResource); + } else { + // We don't need to check whether a preloadResource exists in the renderState + // because if it did exist then the resourceState would also exist and we would + // have hit the primary if condition above. } - stylesInPrecedence.set(key, resource); + + // We add the newly created resource to our StyleQueue and if necessary + // track the resource with the currently rendering boundary + styleQueue.sheets.set(key, resource); if (renderState.boundaryResources) { - renderState.boundaryResources.add(resource); + renderState.boundaryResources.stylesheets.add(resource); } } else { // We need to track whether this boundary should wait on this resource or not. @@ -2197,11 +2272,11 @@ function pushLink( // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. - if (stylesInPrecedence) { - const resource = stylesInPrecedence.get(key); + if (styleQueue) { + const resource = styleQueue.sheets.get(key); if (resource) { if (renderState.boundaryResources) { - renderState.boundaryResources.add(resource); + renderState.boundaryResources.stylesheets.add(resource); } } } @@ -2334,45 +2409,49 @@ function pushStyle( } } - const key = getResourceKey('style', href); - let resource = renderState.stylePrecedences.get(precedence); - if (!resumableState.stylesMap.hasOwnProperty(key)) { - if (!resource) { - resource = { - type: 'style', - chunks: [], - state: NoState, - props: { - precedence, - hrefs: [href], - }, - }; - renderState.stylePrecedences.set(precedence, resource); - const stylesInPrecedence: Map = new Map(); - stylesInPrecedence.set('', resource); - if (__DEV__) { - if (renderState.precedences.has(precedence)) { - console.error( - 'React constructed a new style precedence set when one already exists for this precedence: "%s". This is a bug in React.', - precedence, - ); - } + const key = getResourceKey(href); + let styleQueue = renderState.styles.get(precedence); + const hasKey = resumableState.styleResources.hasOwnProperty(key); + const resourceState = hasKey + ? resumableState.styleResources[key] + : undefined; + if (resourceState !== EXISTS) { + // We are going to create this resource now so it is marked as Exists + resumableState.styleResources[key] = EXISTS; + + if (__DEV__) { + if (resourceState) { + console.error( + '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.', + href, + ); } - renderState.precedences.set(precedence, stylesInPrecedence); + } + + if (!styleQueue) { + // This is the first time we've encountered this precedence we need + // to create a StyleQueue. + styleQueue = { + precedence: stringToChunk(escapeTextForBrowser(precedence)), + rules: ([]: Array), + hrefs: [stringToChunk(escapeTextForBrowser(href))], + sheets: (new Map(): Map), + }; + renderState.styles.set(precedence, styleQueue); } else { - resource.props.hrefs.push(href); + // We have seen this precedence before and need to track this href + styleQueue.hrefs.push(stringToChunk(escapeTextForBrowser(href))); } - resumableState.stylesMap[key] = null; - pushStyleContents(resource.chunks, props); + pushStyleContents(styleQueue.rules, props); } - if (resource) { + if (styleQueue) { // We need to track whether this boundary should wait on this resource or not. // Typically this resource should always exist since we either had it or just created // it. However, it's possible when you resume that the style has already been emitted // and then it wouldn't be recreated in the RenderState and there's no need to track // it again since we should've hoisted it to the shell already. if (renderState.boundaryResources) { - renderState.boundaryResources.add(resource); + renderState.boundaryResources.styles.add(styleQueue); } } @@ -2475,23 +2554,6 @@ function pushStyleContents( return; } -function getImagePreloadKey( - href: string, - imageSrcSet: ?string, - imageSizes: ?string, -) { - let uniquePart = ''; - if (typeof imageSrcSet === 'string' && imageSrcSet !== '') { - uniquePart += '[' + imageSrcSet + ']'; - if (typeof imageSizes === 'string') { - uniquePart += '[' + imageSizes + ']'; - } - } else { - uniquePart += '[][]' + href; - } - return getResourceKey('image', uniquePart); -} - function pushImg( target: Array, props: Object, @@ -2502,7 +2564,9 @@ function pushImg( const {src, srcSet} = props; if ( props.loading !== 'lazy' && - (typeof src === 'string' || typeof srcSet === 'string') && + (src || srcSet) && + (typeof src === 'string' || src == null) && + (typeof srcSet === 'string' || srcSet == null) && props.fetchPriority !== 'low' && pictureTagInScope === false && // We exclude data URIs in src and srcSet since these should not be preloaded @@ -2525,39 +2589,50 @@ function pushImg( ) { // We have a suspensey image and ought to preload it to optimize the loading of display blocking // resumableState. - const {sizes} = props; - const key = getImagePreloadKey(src, srcSet, sizes); - let resource: void | PreloadResource; - if (!resumableState.preloadsMap.hasOwnProperty(key)) { - const preloadProps: PreloadProps = { - rel: 'preload', - as: 'image', - // There is a bug in Safari where imageSrcSet is not respected on preload links - // so we omit the href here if we have imageSrcSet b/c safari will load the wrong image. - // This harms older browers that do not support imageSrcSet by making their preloads not work - // but this population is shrinking fast and is already small so we accept this tradeoff. - href: srcSet ? undefined : src, - imageSrcSet: srcSet, - imageSizes: sizes, - crossOrigin: props.crossOrigin, - integrity: props.integrity, - type: props.type, - fetchPriority: props.fetchPriority, - referrerPolicy: props.referrerPolicy, - }; - resource = { - type: 'preload', - chunks: [], - state: NoState, - props: preloadProps, - }; - resumableState.preloadsMap[key] = preloadProps; - renderState.preloadsMap.set(key, resource); - pushLinkImpl(resource.chunks, preloadProps); - } else { - resource = renderState.preloadsMap.get(key); - } + const sizes = typeof props.sizes === 'string' ? props.sizes : undefined; + const key = getImageResourceKey(src, srcSet, sizes); + + const promotablePreloads = renderState.preloads.images; + + let resource = promotablePreloads.get(key); if (resource) { + // We consider whether this preload can be promoted to higher priority flushing queue. + // The only time a resource will exist here is if it was created during this render + // and was not already in the high priority queue. + if ( + props.fetchPriority === 'high' || + renderState.highImagePreloads.size < 10 + ) { + // Delete the resource from the map since we are promoting it and don't want to + // reenter this branch in a second pass for duplicate img hrefs. + promotablePreloads.delete(key); + + // $FlowFixMe - Flow should understand that this is a Resource if the condition was true + renderState.highImagePreloads.add(resource); + } + } else if (!resumableState.imageResources.hasOwnProperty(key)) { + // We must construct a new preload resource + resumableState.imageResources[key] = PRELOAD_NO_CREDS; + resource = []; + pushLinkImpl( + resource, + ({ + rel: 'preload', + as: 'image', + // There is a bug in Safari where imageSrcSet is not respected on preload links + // so we omit the href here if we have imageSrcSet b/c safari will load the wrong image. + // This harms older browers that do not support imageSrcSet by making their preloads not work + // but this population is shrinking fast and is already small so we accept this tradeoff. + href: srcSet ? undefined : src, + imageSrcSet: srcSet, + imageSizes: sizes, + crossOrigin: props.crossOrigin, + integrity: props.integrity, + type: props.type, + fetchPriority: props.fetchPriority, + referrerPolicy: props.referrerPolicy, + }: PreloadProps), + ); if ( props.fetchPriority === 'high' || renderState.highImagePreloads.size < 10 @@ -2565,6 +2640,9 @@ function pushImg( renderState.highImagePreloads.add(resource); } else { renderState.bulkPreloads.add(resource); + // We can bump the priority up if the same img is rendered later + // with fetchPriority="high" + promotablePreloads.set(key, resource); } } } @@ -2899,33 +2977,48 @@ function pushScript( } const src = props.src; - const key = getResourceKey('script', src); + const key = getResourceKey(src); // We can make this "`, + `"
hello world
"`, ); }); @@ -505,7 +505,7 @@ describe('ReactDOMFizzServerBrowser', () => { ); const result = await readResult(stream); expect(result).toMatchInlineSnapshot( - `"
hello world
"`, + `"
hello world
"`, ); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js index 4a20f1be57f77..f7bab722bb386 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js @@ -98,7 +98,7 @@ describe('ReactDOMFizzServerNode', () => { pipe(writable); jest.runAllTimers(); expect(output.result).toMatchInlineSnapshot( - `"
hello world
"`, + `"
hello world
"`, ); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js index 58e34316b0782..9ec0f2c97c9fb 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js @@ -145,7 +145,7 @@ describe('ReactDOMFizzStaticBrowser', () => { }); const prelude = await readContent(result.prelude); expect(prelude).toMatchInlineSnapshot( - `"
hello world
"`, + `"
hello world
"`, ); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStaticFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStaticFloat-test.js new file mode 100644 index 0000000000000..9a825bf1e3871 --- /dev/null +++ b/packages/react-dom/src/__tests__/ReactDOMFizzStaticFloat-test.js @@ -0,0 +1,275 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +import { + getVisibleChildren, + insertNodesAndExecuteScripts, +} from '../test-utils/FizzTestUtils'; + +// Polyfills for test environment +global.ReadableStream = + require('web-streams-polyfill/ponyfill/es6').ReadableStream; +global.TextEncoder = require('util').TextEncoder; + +let React; +let ReactDOM; +let ReactDOMFizzServer; +let ReactDOMFizzStatic; +let Suspense; +let container; + +describe('ReactDOMFizzStaticFloat', () => { + beforeEach(() => { + jest.resetModules(); + React = require('react'); + ReactDOM = require('react-dom'); + ReactDOMFizzServer = require('react-dom/server.browser'); + if (__EXPERIMENTAL__) { + ReactDOMFizzStatic = require('react-dom/static.browser'); + } + Suspense = React.Suspense; + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + document.body.removeChild(container); + }); + + async function readIntoContainer(stream) { + const reader = stream.getReader(); + let result = ''; + while (true) { + const {done, value} = await reader.read(); + if (done) { + break; + } + result += Buffer.from(value).toString('utf8'); + } + const temp = document.createElement('div'); + temp.innerHTML = result; + await insertNodesAndExecuteScripts(temp, container, null); + } + + // @gate enablePostpone + it('should transfer connection credentials across prerender and resume for stylesheets, scripts, and moduleScripts', async () => { + let prerendering = true; + function Postpone() { + if (prerendering) { + React.unstable_postpone(); + } + return ( + <> + + "`, + `"
hello world
"`, ); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index bff5b6b262e87..8fc6ef45ab2e2 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -4014,6 +4014,87 @@ body { ); }); + it('can promote images to high priority when at least one instance specifies a high fetchPriority', async () => { + function App() { + // If a ends up in a higher priority queue than b it will flush first + ReactDOM.preload('a', {as: 'image'}); + ReactDOM.preload('b', {as: 'image'}); + return ( + + + + + + + + + + + + + + + + + + + + + + + ); + } + + await act(() => { + renderToPipeableStream().pipe(writable); + }); + expect(getMeaningfulChildren(document)).toEqual( + + + {/* The First 10 high priority images were just the first 10 rendered images */} + + + + + + + + + + + {/* The "a" image was rendered a few times but since at least one of those was with + fetchPriorty="high" it ends up in the high priority queue */} + + {/* Stylesheets come in between high priority images and regular preloads */} + + {/* The remainig images that preloaded at regular priority */} + + + + + + + + + + + + + + + + + + + + + + + + , + ); + }); + it('preloads from rendered images properly use srcSet and sizes', async () => { function App() { ReactDOM.preload('1', {as: 'image', imageSrcSet: 'ss1'}); @@ -4119,6 +4200,501 @@ body { ); }); + it('should warn if you preload a stylesheet and then render a style tag with the same href', async () => { + const style = 'body { color: red; }'; + function App() { + ReactDOM.preload('foo', {as: 'style'}); + return ( + + + hello + + + + ); + } + + await expect(async () => { + await act(() => { + renderToPipeableStream().pipe(writable); + }); + }).toErrorDev([ + '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.', + ]); + + expect(getMeaningfulChildren(document)).toEqual( + + + + + + hello + , + ); + }); + + it('should preload only once even if you discover a stylesheet, script, or moduleScript late', async () => { + function App() { + // We start with preinitializing some resources first + ReactDOM.preinit('shell preinit/shell', {as: 'style'}); + ReactDOM.preinit('shell preinit/shell', {as: 'script'}); + ReactDOM.preinitModule('shell preinit/shell', {as: 'script'}); + + // We initiate all the shell preloads + ReactDOM.preload('shell preinit/shell', {as: 'style'}); + ReactDOM.preload('shell preinit/shell', {as: 'script'}); + ReactDOM.preloadModule('shell preinit/shell', {as: 'script'}); + + ReactDOM.preload('shell/shell preinit', {as: 'style'}); + ReactDOM.preload('shell/shell preinit', {as: 'script'}); + ReactDOM.preloadModule('shell/shell preinit', {as: 'script'}); + + ReactDOM.preload('shell/shell render', {as: 'style'}); + ReactDOM.preload('shell/shell render', {as: 'script'}); + ReactDOM.preloadModule('shell/shell render'); + + ReactDOM.preload('shell/late preinit', {as: 'style'}); + ReactDOM.preload('shell/late preinit', {as: 'script'}); + ReactDOM.preloadModule('shell/late preinit'); + + ReactDOM.preload('shell/late render', {as: 'style'}); + ReactDOM.preload('shell/late render', {as: 'script'}); + ReactDOM.preloadModule('shell/late render'); + + // we preinit later ones that should be created by + ReactDOM.preinit('shell/shell preinit', {as: 'style'}); + ReactDOM.preinit('shell/shell preinit', {as: 'script'}); + ReactDOM.preinitModule('shell/shell preinit'); + + ReactDOM.preinit('late/shell preinit', {as: 'style'}); + ReactDOM.preinit('late/shell preinit', {as: 'script'}); + ReactDOM.preinitModule('late/shell preinit'); + return ( + + + + "`, + `"
hello world
"`, ); });