Skip to content

Commit c200aa6

Browse files
committed
improve performance of style tags that use precedence
1 parent bc6184d commit c200aa6

File tree

1 file changed

+78
-10
lines changed

1 file changed

+78
-10
lines changed

packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4451,12 +4451,6 @@ function getStyleKey(href: string) {
44514451
return `href="${limitedEscapedHref}"`;
44524452
}
44534453

4454-
function getStyleTagSelector(href: string) {
4455-
const limitedEscapedHref =
4456-
escapeSelectorAttributeValueInsideDoubleQuotes(href);
4457-
return `style[data-href~="${limitedEscapedHref}"]`;
4458-
}
4459-
44604454
function getStylesheetSelectorFromKey(key: string) {
44614455
return `link[rel="stylesheet"][${key}]`;
44624456
}
@@ -4522,6 +4516,64 @@ function getScriptSelectorFromKey(key: string): string {
45224516
return 'script[async]' + key;
45234517
}
45244518

4519+
// for the sake of performance we want to use insertRule
4520+
// if that fails, fall back to appending textContent (which is slow)
4521+
function appendStyleRule(style: HTMLStyleElement, cssText: string) {
4522+
try {
4523+
if (style.sheet)
4524+
style.sheet.insertRule(cssText, style.sheet.cssRules.length);
4525+
else style.textContent += cssText;
4526+
} catch (e) {
4527+
style.textContent += cssText;
4528+
}
4529+
}
4530+
4531+
const styleNodesByPrecedence = new WeakMap<
4532+
HoistableRoot,
4533+
Map<string, HTMLStyleElement>,
4534+
>();
4535+
const styleNodesByHref = new WeakMap<HoistableRoot, Map<string, Instance>>();
4536+
4537+
// when creating our caches, hydrate with data from the DOM
4538+
// this should only happen once per root
4539+
function getHydratedCaches(hoistableRoot: HoistableRoot) {
4540+
let rootHrefCache = styleNodesByHref.get(hoistableRoot);
4541+
let rootPrecedenceCache = styleNodesByPrecedence.get(hoistableRoot);
4542+
4543+
if (!rootHrefCache) {
4544+
rootHrefCache = new Map<string, Instance>();
4545+
styleNodesByHref.set(hoistableRoot, rootHrefCache);
4546+
4547+
const nodesWithHref = hoistableRoot.querySelectorAll('style[data-href]');
4548+
for (let i = 0; i < nodesWithHref.length; i++) {
4549+
// populate our cache for all hrefs listed in this node's data-href
4550+
const node = nodesWithHref[i];
4551+
const href = node.dataset.href;
4552+
for (let j = 0; j < href.length; j++) {
4553+
const h = href[j];
4554+
if (node instanceof HTMLStyleElement) rootHrefCache.set(h, node);
4555+
}
4556+
}
4557+
}
4558+
4559+
if (!rootPrecedenceCache) {
4560+
rootPrecedenceCache = new Map<string, HTMLStyleElement>();
4561+
styleNodesByPrecedence.set(hoistableRoot, rootPrecedenceCache);
4562+
4563+
const nodesWithPrecedence = hoistableRoot.querySelectorAll(
4564+
'style[data-precedence]',
4565+
);
4566+
for (let i = 0; i < nodesWithPrecedence.length; i++) {
4567+
const node = nodesWithPrecedence[i];
4568+
const precedence = node.dataset.precedence;
4569+
if (node instanceof HTMLStyleElement)
4570+
rootPrecedenceCache.set(precedence, node);
4571+
}
4572+
}
4573+
4574+
return {rootHrefCache, rootPrecedenceCache};
4575+
}
4576+
45254577
export function acquireResource(
45264578
hoistableRoot: HoistableRoot,
45274579
resource: Resource,
@@ -4533,16 +4585,31 @@ export function acquireResource(
45334585
case 'style': {
45344586
const qualifiedProps: StyleTagQualifyingProps = props;
45354587

4536-
// Attempt to hydrate instance from DOM
4537-
let instance: null | Instance = hoistableRoot.querySelector(
4538-
getStyleTagSelector(qualifiedProps.href),
4539-
);
4588+
const {rootHrefCache, rootPrecedenceCache} =
4589+
getHydratedCaches(hoistableRoot);
4590+
4591+
// attempt to hydrate first from our cache, then from the DOM
4592+
// this minimizes the number of times we query the DOM
4593+
let instance: void | Instance = rootHrefCache.get(qualifiedProps.href);
4594+
45404595
if (instance) {
45414596
resource.instance = instance;
45424597
markNodeAsHoistable(instance);
45434598
return instance;
45444599
}
45454600

4601+
// attempt to reuse an existing style node before creating a new one
4602+
// this minimizes the number of style nodes we create, which keeps query selectors faster
4603+
instance = rootPrecedenceCache.get(qualifiedProps.precedence);
4604+
if (instance && instance.isConnected) {
4605+
if (typeof qualifiedProps.children === 'string')
4606+
appendStyleRule(instance, qualifiedProps.children);
4607+
if (!instance.dataset.href.includes(qualifiedProps.href))
4608+
instance.dataset.href += ` ${qualifiedProps.href}`;
4609+
resource.instance = instance;
4610+
return instance;
4611+
}
4612+
45464613
const styleProps = styleTagPropsFromRawProps(props);
45474614
const ownerDocument = getDocumentFromRoot(hoistableRoot);
45484615
instance = ownerDocument.createElement('style');
@@ -4556,6 +4623,7 @@ export function acquireResource(
45564623
// resource.state.loading |= Inserted;
45574624
insertStylesheet(instance, qualifiedProps.precedence, hoistableRoot);
45584625
resource.instance = instance;
4626+
rootPrecedenceCache.set(qualifiedProps.precedence, instance);
45594627

45604628
return instance;
45614629
}

0 commit comments

Comments
 (0)