diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 2d29781dfee..e2105e45420 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -4522,6 +4522,79 @@ function getScriptSelectorFromKey(key: string): string { return 'script[async]' + key; } +// for the sake of performance we want to use insertRule +// if that fails, fall back to appending textContent (which is slow) +function appendStyleRule(style: HTMLStyleElement, cssText: string) { + try { + if (style.sheet) + style.sheet.insertRule(cssText, style.sheet.cssRules.length); + else style.textContent += cssText; + } catch (e) { + style.textContent += cssText; + } +} + +const styleNodesByPrecedence = new WeakMap< + HoistableRoot, + Map, +>(); +const styleNodesByHref = new WeakMap< + HoistableRoot, + Map, +>(); + +function updateHrefCache( + hrefCache: Map, + node: HTMLElement, +) { + const hrefs = node.dataset.href.split(' '); + for (let j = 0; j < hrefs.length; j++) { + const href = hrefs[j]; + if ( + typeof HTMLStyleElement !== 'undefined' && + node instanceof HTMLStyleElement + ) + hrefCache.set(href, node); + } +} + +// when creating our caches, hydrate with data from the DOM +// this should only happen once per root +function getHydratedCaches(hoistableRoot: HoistableRoot) { + let rootHrefCache = styleNodesByHref.get(hoistableRoot); + let rootPrecedenceCache = styleNodesByPrecedence.get(hoistableRoot); + + if (!rootHrefCache) { + rootHrefCache = new Map(); + styleNodesByHref.set(hoistableRoot, rootHrefCache); + + const nodesWithHref = hoistableRoot.querySelectorAll('style[data-href]'); + for (let i = 0; i < nodesWithHref.length; i++) { + updateHrefCache(rootHrefCache, nodesWithHref[i]); + } + } + + if (!rootPrecedenceCache) { + rootPrecedenceCache = new Map(); + styleNodesByPrecedence.set(hoistableRoot, rootPrecedenceCache); + + const nodesWithPrecedence = hoistableRoot.querySelectorAll( + 'style[data-precedence]', + ); + for (let i = 0; i < nodesWithPrecedence.length; i++) { + const node = nodesWithPrecedence[i]; + const precedence = node.dataset.precedence; + if ( + typeof HTMLStyleElement !== 'undefined' && + node instanceof HTMLStyleElement + ) + rootPrecedenceCache.set(precedence, node); + } + } + + return {rootHrefCache, rootPrecedenceCache}; +} + export function acquireResource( hoistableRoot: HoistableRoot, resource: Resource, @@ -4533,11 +4606,38 @@ export function acquireResource( case 'style': { const qualifiedProps: StyleTagQualifyingProps = props; - // Attempt to hydrate instance from DOM - let instance: null | Instance = hoistableRoot.querySelector( + const {rootHrefCache, rootPrecedenceCache} = + getHydratedCaches(hoistableRoot); + + // attempt to hydrate first from our href cache, then from the DOM + // this minimizes the number of times we query the DOM + let instance: null | Instance = + rootHrefCache.get(qualifiedProps.href) || null; + + if (instance) { + resource.instance = instance; + markNodeAsHoistable(instance); + return instance; + } + + // attempt to reuse an existing style node before creating a new one + // this minimizes the number of style nodes we create, which keeps query selectors faster + instance = rootPrecedenceCache.get(qualifiedProps.precedence) || null; + if (instance && instance.isConnected) { + if (typeof qualifiedProps.children === 'string') + appendStyleRule(instance, qualifiedProps.children); + if (!instance.dataset.href.includes(qualifiedProps.href)) + instance.dataset.href += ` ${qualifiedProps.href}`; + resource.instance = instance; + return instance; + } + + // if a style tag with the same href was server-rendered but then suspended, it may not be in our cache + instance = hoistableRoot.querySelector( getStyleTagSelector(qualifiedProps.href), ); if (instance) { + updateHrefCache(rootHrefCache, instance); resource.instance = instance; markNodeAsHoistable(instance); return instance; @@ -4557,6 +4657,9 @@ export function acquireResource( insertStylesheet(instance, qualifiedProps.precedence, hoistableRoot); resource.instance = instance; + rootPrecedenceCache.set(qualifiedProps.precedence, instance); + rootHrefCache.set(qualifiedProps.href, instance); + return instance; } case 'stylesheet': { diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index 7404cec64a0..2a321c2dfe4 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -8314,7 +8314,7 @@ background-color: green; , ); - // Client inserted style tags are not grouped together but can hydrate against a grouped set + // Client inserted style tags are grouped and can also hydrate against an existing grouped set ReactDOMClient.hydrateRoot( document, @@ -8347,11 +8347,8 @@ background-color: green; - -