> DevTools now gives you the list of all the CSS selectors that got calculated by the browser engine during this recalculation operation.
This is cool. The rest of the article IMHO is just an example runthrough of micro-optimisations that nobody reading should adopt into their own practice _unless_ their own perf analysis indicates a problem. It's incredibly unlikely that real-user-experienced UI slowdowns are due to stuff like this.
I helped out with this post a bit - The examples he goes through are taken from actual performance investigations done on Microsoft apps that contributed to this feature being developed.
It’s not trying to say everyone should adopt those optimizations, but intended to be a hands-on “try it out” without the reader needing to have their own app handy :)
Super! And thank you. I love this granularity of tooling now. it’s overwhelming, if anything.
To my original comment, I think I’m just naturally very wary of people losing themselves in the weeds without real cause. If there’s genuine slowdowns being experienced, absolutely dive into it. There are cautionary tales here though. Like when one makes an optimisation based on current browser implementations, only to have it be slower within a couple release cycles because the browsers have updated their implementations. Lots of this occurred with js looping idioms when es5 gained broader usage. All wasted time, in the end. IMHO it’s best to stick to idiomatic patterns and those espoused in the specs themselves. 99% of the time, that’ll cover you.
anytime we can stop the death by a thousand cuts is good news to begin to shape best practices and inform future choices. It's probably more the in aggregate threat than any single choice.
I think some of the shown examples exhibit dark patterns in CSS authorship. For instance, unneccessary nesting happens when you use a CSS preprocessor such as LESS or SASS and basically structure/nest your CSS following the structure of your HTML (instead of writing a CSS file orthogonal to the HTML structure). That will generate bad and bloated CSS code which is slow. This habit exists since >10yrs (as LESS/SASS date back for around ~>10yrs).
The amount of trouble and complication front end devs make for themselves because they refuse to learn CSS is incredible. I've literally heard people call it "raw CSS" as if they were toggling in instructions on the front panel of a minicomputer.
Backend developers can be even worse - I’m thinking of someone who had a Very Serious Java framework they used in business apps, which generated many thousands of lines of redundant CSS basically styling every element so they could avoid learning how selectors or positioning worked. The best part was that this was a bottleneck for every project but they kept insisting it was easier.
It’s a question of respect: if you think CSS is a toy language for your inferiors, you probably aren’t going to learn it very well because you’re not going to be receptive to learning the designers’ intent.
I've never heard "raw CSS" with that connotation. I've usually heard it used to point out that they're writing .css files that are getting directly referenced and aren't getting generated or compiled in a build pipeline, because CSS with extra tooling is so common (for good productivity reasons) these days.
As CSS is a declarative non-turing-complete (in any practical sense) language without any specification for performance details, it’s a job of a CSS engine to care about performance of CSS written in this or that way. Blaming developers for not being aware, bothered or disciplined enough to Use It Properly is antitethical to its design and nature.
That doesn't imply you shouldn't use the existing features of the tool to your advantage. I get the lack of spec, but this isn't 2001 anymore. You can create rules in accordance to general relationships, and not strictly parent child ones.
What if you don’t want general relationships and instead design specific blocks as desired? Coincidentally similar geometry doesn’t mean that it must become a local standard with a name and be factored out. It’s actually a bad practice named “fragile base class”.
> This will also reduce the fraction of developers trying to micro-optimize already fast selectors.
I always feel like browser devs regularly look at the web naively and think "yeah nobody should waste time optimizing for that", but then you get sites having 800kb of CSS and lots of terrible selectors and you have no tools to analyze them besides generic coverage.
Personally, I've never felt overwhelmed by choice when it comes to options regarding optimization. It's fine if it's hidden behind a flag, but "nobody could ever need this" feels weird.
Slightly exaggerated numbers there, but how bad can a css selector really be to the client experience when it's likely based on today's UI dev trends that we're loading 3mb of react + other JS
I wish I was exaggerating, that's something I recently had to deal with :(
You're right, "I need an OS written in JS to run before I can toggle that menu" dwarfs all of it. I'm lucky in that regard, I usually deal with jQuery-based things; you can still trim 95% of the code if you got rid of it (carousels are stopping me, people love carousels), but even if you don't, you land at like 100kb JS and you can defer it.
There's a suggestion at the end (well, they did it, anyway) to apply box-sizing: border-box selectively, only where needed. That was probably smart and worked for these devs.
Don't do this in your codebase. This is maybe the most toxic and chaotic CSS change you can make to a codebase. Some devs will realize it's a box model thing right away. But the others will suffer in silence and confusion for quite a while. Both groups will hate you for this practice.
There really should be a way to tell browsers up front about these things like changing box model for all elements, so that it's not forced to apply it for each element on the page one by one.
While I'm sure there are many optimisations already in place, at least we could avoid this anti-pattern that everybody's doing on web these days.
I had looked for something like this in a recent round of optimization to get a feeling whether it's really worth it optimizing individual css selectors. What I took from (old) articles was that it's generally not and the effects are too small to measure, but maybe they never expected to work on large DOMs with lots of css.
What I'd really like is something that'll run a performance recording and allow me to step into it at any given time to see what HTML + CSS is currently in effect, those things have been the most annoying to debug when styles are applied while content is still being parsed and flex is resizing when new columns appear as nodes are added and causing layout shifts.
Microsoft has a long and infamous history with CSS selectors.
That they start off by saying they parse selectors right to left is shocking, but not surprising.
Not the parent, but I can imagine a pretty fast left-to-right algorithm.
First assemble all the CSS rules into a tree, where each non-leaf node represents a selector and following a path down the tree corresponds to reading a selector from left to right. For example, if there are two rules, ".foo .bar" and ".foo .baz", the tree root would be ".foo", and it would have children ".bar" and ".baz".
Then give each DOM element a pointer to a style tree node. When an element is inserted into the document, set its style tree node by looking for a matching selector among the children of the parent element's style tree node. For example, <div class="foo"> would be associated with the ".foo" style tree node; if <div class="bar"> is created underneath that, the browser looks up ".bar" among that node's children. If there is no match, then use the same style tree node from the parent.
That way you can find the matching CSS rules for each element at the cost of a single lookup per element, regardless of how deeply the rules are nested.
In reality it's more complicated than that, because an element might actually need multiple style tree positions. If you have ".foo .bar" and "div .bar", ".foo" and "div" would naively be two separate tree nodes (each with a ".bar" child node), but <div class="foo"> would have to be associated with both nodes. In other words, the style tree is an NFA. But just like with regular expression matching, it might be faster to convert it to a DFA in advance, i.e. duplicate nodes as necessary so that each element does only need one position in the tree.
And the need to support efficient incremental updates (e.g. an element could change class name, or style rules themselves could be added/removed) makes everything even more complicated.
Still, if you asked me before today to guess how browsers implemented CSS rule matching, that's the algorithm I would have thought of… I wonder why they don't do this. Maybe deeply nested CSS rules are not that common in practice.
Doesn't all browsers do that? I've heard that being the reason for it being some what fast, but also why it's hard to add certain things to css people want, because that would break some optimizations.
31 comments
[ 20.8 ms ] story [ 76.7 ms ] threadThis is cool. The rest of the article IMHO is just an example runthrough of micro-optimisations that nobody reading should adopt into their own practice _unless_ their own perf analysis indicates a problem. It's incredibly unlikely that real-user-experienced UI slowdowns are due to stuff like this.
> In practice, does it matter? Maybe. This heavily depends on the web page
It’s not trying to say everyone should adopt those optimizations, but intended to be a hands-on “try it out” without the reader needing to have their own app handy :)
To my original comment, I think I’m just naturally very wary of people losing themselves in the weeds without real cause. If there’s genuine slowdowns being experienced, absolutely dive into it. There are cautionary tales here though. Like when one makes an optimisation based on current browser implementations, only to have it be slower within a couple release cycles because the browsers have updated their implementations. Lots of this occurred with js looping idioms when es5 gained broader usage. All wasted time, in the end. IMHO it’s best to stick to idiomatic patterns and those espoused in the specs themselves. 99% of the time, that’ll cover you.
"Here's a new dev tool that can point to problems, and here's how you fix it"
It’s a question of respect: if you think CSS is a toy language for your inferiors, you probably aren’t going to learn it very well because you’re not going to be receptive to learning the designers’ intent.
I think Chromium specifically got rid of a tool like this in "Drop the CSS selector profiler"
https://bugs.chromium.org/p/chromium/issues/detail?id=265486
I always feel like browser devs regularly look at the web naively and think "yeah nobody should waste time optimizing for that", but then you get sites having 800kb of CSS and lots of terrible selectors and you have no tools to analyze them besides generic coverage.
Personally, I've never felt overwhelmed by choice when it comes to options regarding optimization. It's fine if it's hidden behind a flag, but "nobody could ever need this" feels weird.
You're right, "I need an OS written in JS to run before I can toggle that menu" dwarfs all of it. I'm lucky in that regard, I usually deal with jQuery-based things; you can still trim 95% of the code if you got rid of it (carousels are stopping me, people love carousels), but even if you don't, you land at like 100kb JS and you can defer it.
His talk here is also a great deep dive - https://nolanlawson.com/2023/01/17/my-talk-on-css-runtime-pe...
Don't do this in your codebase. This is maybe the most toxic and chaotic CSS change you can make to a codebase. Some devs will realize it's a box model thing right away. But the others will suffer in silence and confusion for quite a while. Both groups will hate you for this practice.
While I'm sure there are many optimisations already in place, at least we could avoid this anti-pattern that everybody's doing on web these days.
What I'd really like is something that'll run a performance recording and allow me to step into it at any given time to see what HTML + CSS is currently in effect, those things have been the most annoying to debug when styles are applied while content is still being parsed and flex is resizing when new columns appear as nodes are added and causing layout shifts.
First assemble all the CSS rules into a tree, where each non-leaf node represents a selector and following a path down the tree corresponds to reading a selector from left to right. For example, if there are two rules, ".foo .bar" and ".foo .baz", the tree root would be ".foo", and it would have children ".bar" and ".baz".
Then give each DOM element a pointer to a style tree node. When an element is inserted into the document, set its style tree node by looking for a matching selector among the children of the parent element's style tree node. For example, <div class="foo"> would be associated with the ".foo" style tree node; if <div class="bar"> is created underneath that, the browser looks up ".bar" among that node's children. If there is no match, then use the same style tree node from the parent.
That way you can find the matching CSS rules for each element at the cost of a single lookup per element, regardless of how deeply the rules are nested.
In reality it's more complicated than that, because an element might actually need multiple style tree positions. If you have ".foo .bar" and "div .bar", ".foo" and "div" would naively be two separate tree nodes (each with a ".bar" child node), but <div class="foo"> would have to be associated with both nodes. In other words, the style tree is an NFA. But just like with regular expression matching, it might be faster to convert it to a DFA in advance, i.e. duplicate nodes as necessary so that each element does only need one position in the tree.
And the need to support efficient incremental updates (e.g. an element could change class name, or style rules themselves could be added/removed) makes everything even more complicated.
Still, if you asked me before today to guess how browsers implemented CSS rule matching, that's the algorithm I would have thought of… I wonder why they don't do this. Maybe deeply nested CSS rules are not that common in practice.