Where CSS Performance Problems Come From
CSS performance advice sometimes fixates on whether a descendant selector contains one class too many. Modern browsers are very good at matching ordinary selectors, and most sites have larger problems worth measuring first.
Stylesheet weight, render blocking, repeated layout, large DOMs, and expensive visual effects are much more likely to matter than one sensible use of :has().
Start with the amount of CSS shipped
A 10 KB stylesheet and a 500 KB stylesheet give the browser different amounts of work:
download
↓
parse
↓
style rules to manage
Removing rules for deleted components is often the easiest improvement. A framework bundle containing styles for 500 components is wasteful when the site uses 20 of them.
Stylesheets in the document head are normally render-blocking because the browser needs CSS before it can paint the page correctly. That is not an argument against CSS; it is a reason to keep the critical stylesheet focused and deliver it quickly.
Layout and paint can cost more than matching
Changing layout properties during an animation can require repeated layout work:
width
height
top
left
When the effect allows it, animating these is often cheaper:
transform
opacity
The worst cases frequently involve JavaScript changing a layout-related style and immediately reading geometry in a loop. That can force the browser to recalculate layout repeatedly. Blaming the selector misses the interaction causing the work.
Painting can be expensive as well. A large fixed layer with:
backdrop-filter: blur(40px);
may cost much more than a solid background, particularly on a less capable device. Large blurred shadows and filters deserve testing where they are actually used.
Simple selectors are still a good idea
This selector is more coupled to the document structure:
.main .card .title {
...
}
than this one:
.card-title {
...
}
I prefer the simpler version for maintenance. On a normal site, rewriting every selector for a theoretical matching improvement is rarely the best performance work.
The same restraint applies to :has():
.card:has(img) {
...
}
Browsers have optimized selector matching heavily. Extreme selectors across an enormous DOM can cost more, but normal component logic should be judged with a performance trace rather than folklore.
The DOM and related assets are part of the picture
CSS must match against elements, so a document with tens of thousands of nodes creates more work than a small one. Simplifying unnecessary wrappers may help both style calculation and maintainability.
CSS can also initiate other downloads. An @font-face rule may be tiny while its font files add hundreds of kilobytes. The cost comes from the complete loading strategy, not only the text inside the stylesheet. System fonts remove those requests, although they are not the right design choice for every site.
Optimize in an order that can change the result
My order is:
- Remove unused CSS.
- Keep the main stylesheet reasonably small.
- Check repeated layout and heavy paint effects.
- Prefer
transformandopacitywhen they fit the animation. - Keep the DOM focused.
- Measure before rewriting selectors.
A small static site with 20 KB of CSS does not need a week of selector benchmarks. Clear CSS, a manageable stylesheet, and tests on devices representative of the audience are usually the better investment.