8000 improve performance of style tags that use precedence by RJWadley · Pull Request #32957 · facebook/react · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

improve performance of style tags that use precedence #32957

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

RJWadley
Copy link
@RJWadley RJWadley commented Apr 18, 2025

Summary

Currently, every time we insert a new style tag that has precedence, we first need to run 2 querySelectorAll calls. If we want to render 400 style tags, we'll need to run querySelectorAll 800 times. This is fairly slow.

Additionally, every time we encounter a new style tag react will create a new style tag in the DOM, and will (as far as I can tell) make no attempt to remove that style tag at any point. So the process of inserting itself will get slower as we insert more and more styles.

We can address the performance in this specific case by inserting nodes less often:

  • keep track of all our style tags based on their data-href and data-precedence values
  • the first time we acquire a style resource, query the DOM to populate our cache with any tags that may have been server rendered
  • if we already have a style node for a given href, we never need to insert a new style tag
  • if we already have a style node for a given precedence, we can append our new rule to the existing style tag instead of creating a new node
  • when possible, append new styles using insertRule to minimize the amount of parsing the browser needs to do. fall back to appending our style if needed (which is slow, but might be needed if inserting fails)

a few other random notes

  • this optimization is aimed at inserting nodes less often, but the actual process of inserting nodes is unchanged and still not that efficient. If you were to, for example, insert 400 stylesheet resources instead of 400 style resources you would encounter a performance issue (because those cannot be optimized in this way)
  • I would be interested to see react manage these styles better. perhaps using insertRule and deleteRule to remove/update styles rather than inserting them into the DOM forever. that would be beyond the scope of this PR, of course, but it bothers me that react makes no attempt to clean up these styles when components are unmounted.

fixes #32806
fixes #30739

How did you test this change?

I've built this version of react and patched it into a few projects that were having performance issues. I'm planning on doing this again (I've tweaked a few things) and will update these results when I've done that.

in one such expensive render:
the entire process (including the browsers style recalculation) went from 2.11s to 504.34ms
the react portion of that process went from 1.47s to 26.91ms
flushMutationEffects (the previously slow part) went from 1.45s to 7.58ms

in one more extreme case:
the entire process (including the browsers style recalculation) went from 9.57s to 600.18ms
the react portion of that process went from 8.63s to 41.05ms
flushMutationEffects (the previously slow part) went from 8.61s to 6.67ms

These two examples are obviously not fully comprehensive (I could fabricate a fairly comprehensive example case if anyone is interested) but they do roughly represent the level of improvement I've seen in my testing.

@RJWadley RJWadley marked this pull request as ready for review April 18, 2025 17:24
@react-sizebot
Copy link

Comparing: bc6184d...7c917f5

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.js = 6.68 kB 6.68 kB = 1.83 kB 1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js +0.57% 516.14 kB 519.07 kB +0.63% 91.87 kB 92.44 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.69 kB 6.69 kB = 1.83 kB 1.83 kB
8000 oss-experimental/react-dom/cjs/react-dom-client.production.js +0.47% 621.45 kB 624.37 kB +0.51% 110.00 kB 110.57 kB
facebook-www/ReactDOM-prod.classic.js +0.45% 654.82 kB 657.75 kB +0.46% 115.57 kB 116.10 kB
facebook-www/ReactDOM-prod.modern.js +0.45% 645.10 kB 648.03 kB +0.47% 114.04 kB 114.57 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable-semver/react-dom/cjs/react-dom-client.production.js +0.57% 516.02 kB 518.94 kB +0.63% 91.84 kB 92.42 kB
oss-stable/react-dom/cjs/react-dom-client.production.js +0.57% 516.14 kB 519.07 kB +0.63% 91.87 kB 92.44 kB
facebook-react-native/react-dom/cjs/ReactDOMClient-prod.js +0.54% 540.22 kB 543.14 kB +0.60% 95.76 kB 96.34 kB
facebook-react-native/react-dom/cjs/ReactDOMProfiling-prod.js +0.54% 545.73 kB 548.65 kB +0.59% 96.84 kB 97.41 kB
oss-stable-semver/react-dom/cjs/react-dom-profiling.profiling.js +0.53% 548.62 kB 551.54 kB +0.60% 96.70 kB 97.28 kB
oss-stable/react-dom/cjs/react-dom-profiling.profiling.js +0.53% 548.74 kB 551.66 kB +0.60% 96.73 kB 97.31 kB
facebook-react-native/react-dom/cjs/ReactDOMClient-profiling.js +0.52% 567.26 kB 570.19 kB +0.57% 99.68 kB 100.24 kB
facebook-react-native/react-dom/cjs/ReactDOMProfiling-profiling.js +0.51% 573.20 kB 576.13 kB +0.55% 100.84 kB 101.39 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js +0.47% 621.45 kB 624.37 kB +0.51% 110.00 kB 110.57 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.production.js +0.46% 635.86 kB 638.78 kB +0.50% 113.57 kB 114.13 kB
facebook-www/ReactDOM-prod.modern.js +0.45% 645.10 kB 648.03 kB +0.47% 114.04 kB 114.57 kB
facebook-www/ReactDOM-prod.classic.js +0.45% 654.82 kB 657.75 kB +0.46% 115.57 kB 116.10 kB
facebook-www/ReactDOMTesting-prod.modern.js +0.44% 659.50 kB 662.43 kB +0.49% 117.64 kB 118.21 kB
facebook-www/ReactDOMTesting-prod.classic.js +0.44% 669.22 kB 672.15 kB +0.46% 119.25 kB 119.79 kB
oss-experimental/react-dom/cjs/react-dom-profiling.profiling.js +0.43% 686.17 kB 689.09 kB +0.46% 119.79 kB 120.35 kB
facebook-www/ReactDOM-profiling.modern.js +0.41% 718.80 kB 721.73 kB +0.42% 124.30 kB 124.83 kB
facebook-www/ReactDOM-profiling.classic.js +0.40% 726.85 kB 729.77 kB +0.42% 125.62 kB 126.14 kB
oss-stable-semver/react-dom/cjs/react-dom-client.development.js +0.35% 956.50 kB 959.84 kB +0.36% 161.16 kB 161.74 kB
oss-stable/react-dom/cjs/react-dom-client.development.js +0.35% 956.63 kB 959.96 kB +0.36% 161.19 kB 161.77 kB
oss-stable-semver/react-dom/cjs/react-dom-profiling.development.js +0.34% 972.94 kB 976.28 kB +0.36% 164.00 kB 164.59 kB
oss-stable/react-dom/cjs/react-dom-profiling.development.js +0.34% 973.07 kB 976.40 kB +0.36% 164.03 kB 164.63 kB
facebook-react-native/react-dom/cjs/ReactDOMClient-dev.js +0.34% 989.84 kB 993.18 kB +0.35% 166.45 kB 167.03 kB
facebook-react-native/react-dom/cjs/ReactDOMProfiling-dev.js +0.33% 1,006.17 kB 1,009.50 kB +0.35% 169.27 kB 169.86 kB
oss-experimental/react-dom/cjs/react-dom-client.development.js +0.30% 1,129.72 kB 1,133.06 kB +0.31% 188.57 kB 189.16 kB
oss-experimental/react-dom/cjs/react-dom-profiling.development.js +0.29% 1,146.12 kB 1,149.45 kB +0.31% 191.41 kB 192.00 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.development.js +0.29% 1,146.27 kB 1,149.60 kB +0.30% 192.26 kB 192.84 kB
facebook-www/ReactDOM-dev.modern.js +0.28% 1,183.58 kB 1,186.92 kB +0.30% 196.27 kB 196.86 kB
facebook-www/ReactDOM-dev.classic.js +0.28% 1,192.72 kB 1,196.06 kB +0.30% 198.05 kB 198.64 kB
facebook-www/ReactDOMTesting-dev.modern.js +0.28% 1,200.12 kB 1,203.45 kB +0.30% 200.11 kB 200.71 kB
facebook-www/ReactDOMTesting-dev.classic.js +0.28% 1,209.26 kB 1,212.59 kB +0.30% 201.82 kB 202.43 kB

Generated by 🚫 dangerJS against 7c917f5

Comment on lines +4527 to +4535
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;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please take my feedback with a grain of salt. I'm not a React team member - but we were chatting with @gnoff about most of this already.

I think this change should be reverted (for now). I totally agree this is something that will have to be implemented in some capacity at some point but there are open design questions around this.

It feels like, at the moment, this breaks the core assumption that a single resource is represented by a single element. OTOH, not many tests were affected by this change and that makes me doubt a little bit in my interpretation of the change.

All in all, I'd focus first on caching insertion points for each precedence to avoid expensive and redundant querying.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be interested to see react manage these styles better. perhaps using insertRule and deleteRule to remove/update styles rather than inserting them into the DOM forever. that would be beyond the scope of this PR, of course, but it bothers me that react makes no attempt to clean up these styles when components are unmounted.

I could imagine many cases where a component gets mounted & unmounted in a cycle, and that would have React adding & removing the styles in a cycle as well, wasting CPU time. Leaving styles forever is not a bad option.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, that's fair - it's definitely faster at first to leave them, but it gets slower with time if you don't clean up.
my main issue with leaving styles in is it causes headaches in development.

imagine you write this during development:

<style href="sample" precedence="low">
    {" h1 { color: red; } "}
</style>

but nah, you've changed your mind. red isn't your color. you replace it:

<style href="sample" precedence="low">
    {" h1 { color: blue; } "}
</style>

what color do you think paragraphs will be now? If you've read the docs and know a bit about how CSS works you probably know. I find this to be expected, but a bit unintuitive and definitely annoying.

Copy link
Contributor
@romgrk romgrk May 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is one case where dev should behave differently than prod, imo. Dev should probably insert/remove whole style tags at once, removing old ones. Prod should probably be optimized for an append-only use-case, possibly with insertRule.

The deleteRule API is also annoying to use, it uses a rule index rather than a rule id, so managing rules is not an O(1) operation once the style rules array becomes holey (or in other words, when you need to delete rules in the middle of the stylesheet). I don't think it's particularly well suited for high-perf.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be possible to insert/remove only when a hot reload cycle has just passed, and for the other interactions, retain the deduplication behavior.

For instance, in react-refresh runtime, one could keep a global key for the whole app that gets regenerated every time any module with components hot reloads. Associate this key with each injected styles. When deduping on href/precendce, compare the styles key with the global one to know if they should be recreated or deduped.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
6 participants
0