Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
My talk on CSS runtime performance (nolanlawson.com)
114 points by luu on March 7, 2024 | hide | past | favorite | 17 comments


Wow this was a great talk. I hardly ever manage to watch videos of talks through to the end anymore, for reasons of internet social media attention span etc etc. But this one just sucked me right in. I didn't even know that I didn't know what the browser is doing when it's "styling", love it.


Agreed, it's so worth the watch. Thank you Nolan, if you happen to see this!


Thanks a lot! I was just trying to highlight the cool work that browsers do under the hood, but which wasn't very well-known. So I'm really glad that folks enjoyed the talk. :)


I wish these CSS selector details were better documented somewhere. For example, if right to left is always true, then

.cls[attr="value"] is always slower than [attr="value"].cls

But I never see anybody using the second one.

I think it's also interesting that there is no way to do figcaption.cls inverted like .clsfigcaption.

It's strange that default CSS that comes with tools like Wordpress abuses pseudo-selectors as much. I think everyone just writes it off as "it's the CSS 3 way."

On interesting example is that some javascript libraries to highlight code syntax will use ancestor selectors like .highlighted .keyword, which in my mind is crazy. You are making the classes through javascript, you could just use a single glass, which is what Highlight.js does: its classes look like .hlts-kw.

One time I had to deal with layout in CSS being too slow. It was not because of webpage, but an electron app for android that needed to get the bounding rectangle a lot to place divs for its custom GUI elements on screen. Since changing the DOM/CSS changes the bounding rects, placing a div and then checking the bounding rect again was a performance cost. The solution ended up being caching the client rect once before updating the DOM to avoid the thrashing.

Anyway, great talk :D I hope more people become aware CSS isn't magic, nor a black box, so they can think twice before writing their selectors. Sometimes I see CSS code with superfluous nested ancestors (e.g. .wrapper .content .post-body img), and I think that happens because it really just works, but unfortunately it seems that mentally carries on after a developer stops being a beginner, because the CSS just keeps working anyway.


Per your example I wonder if CSS engine devs optimize for most common use like many JS engines do. For example JS `for` loops, `for (let i=maxVal;i--;)` should hypothetically perform better than `for (let i=0;i<=maxVal;i++)` but they are pretty much equal in modern interpreters.


>.cls[attr="value"] is always slower than [attr="value"].cls

I understood the right to left was on the element level and not on the node level so I would think attr="value wouldn't have an effect.


That makes sense but is it really true? I feel it's weird that this is such a black box.


I don't think it is any more a black box than JavaScript and the DOM, it just happens that most people don't take the effort to learn it that they spend on the other stuff, and unless those people are super geniuses I'm sure there are still parts to the technologies they have bothered to learn that surprise them or they are unsure about and would have to check from time to time.


I think it would be better to compare it to SQL. The RDBMS does a lot of stuff in the background with the query planner, but it's very easy to get information about implementation detail. It's not hard for someone who just writes SQL to learn how indexes make rows faster, how btree indexes work, and why using asc/desc in a multi-column index matters. With CSS you never get any of that, so I think it's a much blacker box than normal.


I've worked on both an RDBMS optimizer and a CSS engine, and while the comparison is interesting, it's also not quite right. In particular, you can write pretty crappy CSS and it will work quite alright, including in all major browsers. Database engines are not like that; there are more ways of doing everything, the bad choices are _really_ bad, and the heuristics much more prone to suddenly do really slow things based on reasonable queries. Most optimization differences between versions and engines are about constant factors and not radically different algorithms (although for some select topics, like invalidation, you may get burned). Even if you write a truly horrible CSS rule, it is unlikely to freeze your page for a minute in any current browser. This is not because browser engineers are more talented than RDBMS engineers, it's just a much simpler problem.

You're right in that there's less information out there for CSS engines (from flipping through slides on this talk, it looks like a great way to improve on that situation!), but there's just a lot more to _say_ about SQL performance. And a lot of what's being said about the latter is honestly pretty sketchy :-)


> .cls[attr="value"] is always slower than [attr="value"].cls

Hey, have you instrumented this by chance? I assumed browsers have pretty smart query optimizers by now.


I cannot speak for Firefox or WebKit, but at least in Chromium, these are going to be pretty much identical. Both are going to be bucketed based on the class (i.e., would only be considered for an element with class="cls", based on a hash table lookup), and then the [attr="value"] would be checked the obvious way.

The reason why the right-to-left rule makes sense _between compounds in a complex selector_ (i.e., “the parts that are separated by a space”, for an imprecise term) is that this is the part that applies to the element you are computing style for right now. I.e., say that you have a rule like

  .a.b span { color: whatever; }
This rule will apply to a <span> (the span is the “subject” of the rule) that has an .a.b element somewhere up higher in the tree. If you have made the rule specific enough that it rejects on the subject (say, you're computing style right now for a <h1>, not a <span>), you can skip the entire rule right away. (Most rules do, after all, not match most elements, so rejection is what you want to optimize for.) But once that “span” selector matches on some element, you need to walk the ancestor chain from that element, potentially every single ancestor, to verify that there's no parent that has both classes .a and .b, before you can reject the rule. So rejecting on the subject (save for :has(), which is a story in its own) needs to look at one element, rejecting on something non-subject is much worse. (The Bloom filter would stop early if you don't have at least something with .a and something with .b in your tree, a so-called “fast reject”, but it can't distinguish <div class="a"><div class="b"><span> from <div class="a b"><span>.)

The best thing for selector performance _by far_ is to write the subject so that most elements don't even see it, by means of getting it into the right hash table bucket. A selector like “#xyz” is going to be _so_ much faster than “#xyz *”. But of course, most sites are not really style bound; CSS performance is something you should probably look at after you have your HTTP and JavaScript issues under control.

(I work on Chromium style performance)


Very interesting, thank you!


No, I haven't. But I'm assuming it is based on the "right to left" concept while you are assuming it isn't based on browsers' ability to optimize this. The spec says nothing about it and I have never seen any documentation saying "this case will be optimized." And this is the problem with it, imho. Why would this be optimized but not .cls :has(.foo) .bar? Is :nth-child optimized? Should I do [attr]:nth-child or :nth-child:is([attr])? There are several ways to write the same complex selector and a lack of first-party guidelines about the best way to do it.

Edit: to illustrate my point better, it used to be that if you wanted to make a table with alternating row colors you just used a class on each row. Then :nth-child(odd) and :nth-child(even) came out and everyone was like "just use that instead! No more classes in every row!" And now we have .foo-table :nth-child(odd) > td because obviously there's a tbody/thead/tfoot between tr and table and nobody knows if you should do this or .foo-table > :is(thead, tfoot, tbody) > :nth-child(odd) > td or which one is faster or if you should just use .odd > td because HTML TABLES ARE THE REAL EVIL TABLES IN WEB HISTORY. CSS TABLES WERE SAINTS COMPARED TO THIS. Anyway, every browser has different optimizations, and I have 5 different ways to make a row color alternate. This is the problem. When we have actual complex styling that makes use of the most powerful CSS selectors, all common optimization for simple stuff fall apart. So why even use powerful CSS selectors when they have potential to be permanently slow on HTML code that you may not ever be able to change in the future?


> …while you are assuming it isn't…

I'm honestly just professing ignorance! Along with generally wondering about the state of CSS query optimizers, I think the two have the same specificity, which (in my very-incomplete mental model) makes me think that they're about the same amount of work to resolve.

If Nolan stops by maybe he can weigh in. :^)


I just have this vague sense of dread about the tens of thousands of style rules trying to match on each element.


It never came to my mind to think about CSS from this perspective. Very useful information.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: