From 7eafe3db677fb19fbc319885a948388ffce8be11 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Wed, 20 Sep 2023 21:29:33 -0700 Subject: [PATCH] [Fizz][Float] Refactor Resources 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 Precedence. 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 | 576 +++++++++--------- 1 file changed, 273 insertions(+), 303 deletions(-) diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 9eb6780fe5de1..a0ade1769295e 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -148,17 +148,17 @@ 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, - bootstrapScripts: Set, - scripts: Set, - bulkPreloads: Set, + bootstrapScripts: Set, + scripts: Set, + bulkPreloads: Set, // Temporarily keeps track of key to preload resources before shell flushes. - preloadsMap: Map, + preloadsMap: Map, // Module-global-like reference for current boundary resources boundaryResources: ?BoundaryResources, @@ -188,10 +188,9 @@ export type ResumableState = { hasHtml: boolean, // Resources - Request local cache - preloadsMap: {[key: string]: PreloadProps}, + preloadsMap: {[key: string]: null}, preconnectsMap: {[key: string]: null}, - stylesMap: {[key: string]: null}, - scriptsMap: {[key: string]: null}, + resourcesMap: {[key: string]: null | AdoptablePreloadPropsTuple}, }; const dataElementQuotedEnd = stringToPrecomputedChunk('">'); @@ -497,8 +496,7 @@ export function createResumableState( // persistent preloadsMap: {}, preconnectsMap: {}, - stylesMap: {}, - scriptsMap: {}, + resourcesMap: {}, }; } @@ -2101,7 +2099,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,32 +2134,12 @@ function pushLink( } 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; - } - } - const resource = { - chunks: ([]: Array), - state, - props: resourceProps, - }; - resumableState.stylesMap[key] = null; + const maybePreloadProps = resumableState.resourcesMap.hasOwnProperty( + key, + ) + ? resumableState.resourcesMap[key] + : undefined; + if (maybePreloadProps !== null) { if (!stylesInPrecedence) { stylesInPrecedence = { precedence: stringToChunk(escapeTextForBrowser(precedence)), @@ -2171,6 +2149,33 @@ function pushLink( }; renderState.precedences.set(precedence, stylesInPrecedence); } + + // We are going to create a stylesheet resource so we do not want any preload resources + // to be created after this + resumableState.preloadsMap[key] = null; + + const resource: StylesheetResource = { + state: PENDING, + props: stylesheetPropsFromRawProps(props), + }; + + if (maybePreloadProps) { + const preloadProps: AdoptablePreloadPropsTuple = maybePreloadProps; + adoptPreloadProps(resource.props, preloadProps); + + const preloadResource = renderState.preloadsMap.get(key); + if (preloadResource && preloadResource.length > 0) { + // Preload has not flushed yet, we'll clear the chunks to avoid flushing. + preloadResource.length = 0; + } else { + // Either the preload resource from this render already flushed or a preload + // from a prerender flushed already. Either way we want to avoid flushing a + // preload for the stylesheet + resource.state = PRELOADED; + } + } + + resumableState.resourcesMap[key] = null; stylesInPrecedence.sheets.set(key, resource); if (renderState.boundaryResources) { renderState.boundaryResources.sheets.add(resource); @@ -2318,9 +2323,9 @@ function pushStyle( } } - const key = getResourceKey('style', href); + const key = getResourceKey(href); let stylesInPrecedence = renderState.precedences.get(precedence); - if (!resumableState.stylesMap.hasOwnProperty(key)) { + if (!resumableState.resourcesMap.hasOwnProperty(key)) { if (!stylesInPrecedence) { stylesInPrecedence = { precedence: stringToChunk(escapeTextForBrowser(precedence)), @@ -2334,7 +2339,7 @@ function pushStyle( stringToChunk(escapeTextForBrowser(href)), ); } - resumableState.stylesMap[key] = null; + resumableState.resourcesMap[key] = null; pushStyleContents(stylesInPrecedence.rules, props); } if (stylesInPrecedence) { @@ -2447,23 +2452,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, @@ -2474,7 +2462,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 @@ -2497,9 +2487,9 @@ 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; + const sizes = typeof props.sizes === 'string' ? props.sizes : undefined; + const key = getImageResourceKey(src, srcSet, sizes); + let resource: void | Resource; if (!resumableState.preloadsMap.hasOwnProperty(key)) { const preloadProps: PreloadProps = { rel: 'preload', @@ -2517,14 +2507,10 @@ function pushImg( fetchPriority: props.fetchPriority, referrerPolicy: props.referrerPolicy, }; - resource = { - chunks: [], - state: NoState, - props: preloadProps, - }; - resumableState.preloadsMap[key] = preloadProps; + resource = []; + resumableState.preloadsMap[key] = null; renderState.preloadsMap.set(key, resource); - pushLinkImpl(resource.chunks, preloadProps); + pushLinkImpl(resource, preloadProps); } else { resource = renderState.preloadsMap.get(key); } @@ -2870,32 +2856,35 @@ function pushScript( } const src = props.src; - const key = getResourceKey('script', src); + const key = getResourceKey(src); // We can make this