Closed byung-woo closed 6 months ago
I've chatted with @byung-woo on slack about this before. In general the feature looks nice of course, but the concerns about implementability are still pretty much a thing.
The explainer focuses mostly on Chromium details, which don't apply to Gecko (nor WebKit for that matter). Also the Complex cases section doesn't look super-encouraging.
Apart from the obvious "it's going to be a tool that authors will inevitably shoot themselves in the foot with, and they'll blame the browser engine for it", which may be possibly mitigated in some way, the main problem with :has
for Gecko is that implementing sane style invalidation for it requires basically making our style invalidation machinery not parallel (because :has
can cause us to invalidate upwards the tree, unlike anything else), and also not part of the style recalculation traversal at all (because the only point in which you can invalidate :has
accurately is at DOM mutation time). As a result, that makes me concerned about performance impact specially around DOM mutation, which is not something you seem to have measured and I'd be curious to see.
It also breaks various optimizations that we have to limit the style recalc to a subtree which Blink also has, btw. How do you plan to deal with this? I see no mention of StyleRecalcRoot
or StyleTraversalRoot
(which are the relevant data structures in Blink) or anything similar in the explainer, but this seems like it'd interact with those optimizations...
Thanks you for your feedback, and I'm happy to discuss with you about the details. :)
1.
The explainer focuses mostly on Chromium details, which don't apply to Gecko (nor WebKit for that matter).
Yes I can totally understand about this, and I'm really sorry that I didn't try and don't have solutions for the other engines yet. It would be perfect to provide a magic solution for all the engines. But it looks impossible as you know, so I thought that solving issues one-by-one and trying to find a way to extend those are the only possible way as for now. Could you please understand the current solution starting with Blink in this context? I really hope that this try (and the further tries and discussions) can help to have solutions for other engines also.
2.
... As a result, that makes me concerned about performance impact specially around DOM mutation, which is not something you seem to have measured and I'd be curious to see.
I think this test result looks provides the performance impact relatively.
The test compares the 'invalidation time` (time to reach elements to invalidate) for the two cases with same tree height.
a
to/from an element for .b:has(.a) {...}
c
to/from an element for .c .d {...}
And as you can see from the result, :has
invalidation is much faster.
You can compare with this result.
The test compares the 'invalidation time' + 'recalculation time'(time to match the selector on the invalidated element to check whether the element is subject or not) for the two cases with same tree height
a
to/from an element for .b:has(.e .a) {...}
c
to/from an element for .c .e .d {...}
And as you can see from the result, :has
invalidation+recalculation is slower, but the difference looks not big (less than 10 microseconds).
With the result, I thought that performance impact on DOM mutation is not critical and acceptable. What will be the missing point? Could you please let me know if there is any missing points? It would be really helpful to have any idea to check or test those.
3.
It also breaks various optimizations that we have to limit the style recalc to a subtree which Blink also has, btw. How do you plan to deal with this? I see no mention of StyleRecalcRoot or StyleTraversalRoot (which are the relevant data structures in Blink) or anything similar in the explainer, but this seems like it'd interact with those optimizations...
Basically, as described here and here, I think that the approach doesn't break any optimizations for the original invalidation because it only calls methods of scheduling the original invalidation. I didn't care about the 'StyleRecalcRoot' or 'StyleTraversalRoot' directly because it was hided behind the triggering method. I think I can say that, the current :has
approach generates kind of virtual DOM mutations from a DOM mutation, and feed those to the original invalidation approach. When a DOM change can affect subject elements of a style rule, the :has
invalidation approach finds :has
matching element first, and passes the element to the original invalidation logic as if some DOM change for the style rule occurred in that element.
I'm not sure that my understanding and the explanation is clear. Could you please let me know any doubt or missing point?
Just looking at some stuff for chromestatus and sending a ping here to note that it seems this was discussed before WebKit quickly implemented :has
and is currently in 15.4 beta
This is a very useful feature and worth prototyping. Emilio raised some open spec issues in https://groups.google.com/a/chromium.org/g/blink-dev/c/bRsbl3wLuyk/m/mAwyQB1kBQAJ which are resulting in interop issues between what's already shipping in WebKit and the implementation in Blink, so would like to see those be resolved.
the main problem with :has for Gecko is that implementing sane style invalidation for it requires basically making our style invalidation machinery not parallel (because :has can cause us to invalidate upwards the tree, unlike anything else),
This sounds kinda scary.
Yeah, in general, I suspect to implement this we'd need to rework how a fair amount of our style invalidation works. Specially if we don't want to write tons of :has
-specific invalidation code.
Gecko has more precise style invalidation than Blink, and coalesces invalidations better than both Blink and WebKit. If we want to preserve precise invalidation we need to either do invalidation at DOM mutation time (which breaks the coalescing) or do some kind of fuzzy invalidation like Blink does (which honestly I'm not a fan of, because it creates performance cliffs...).
@byung-woo is my following understanding of the Blink invalidation story for :has()
correct? This and callers is the code I'm looking at:
:has()
selector was tried against a given element (regardless of whether it matches), and store it in a variety of flags (some of them for some special pseudo-classes like :hover
/ :focus
/ etc).:has()
. This has some other sanity-checks like storing all classes and so that are used inside :has()
to avoid work in a variety of common cases, is that right?:has()
is used, you need to propagate the affected flags on DOM mutations potentially to all the inserted subtree, so that they remain correct.If so, I'm still concerned about the poor worst-case performance to be honest. Maybe that's ok... I'd be quite sad of losing precise style invalidation in Firefox. Maybe we can do the "fuzzy" thing just for :has()
, but... I think it's a matter of time that people put really complex stuff with :has()
in their stylesheets in combination with mutations which fall off the happy path, but maybe I'm a bit too pessimist in that regard. :)
Anyways, I guess I wouldn't object to marking this as worth prototyping, but I'm a bit concerned about providing a performance footgun. Author interest is there, for sure.
Here's what Chrome does for :has()
invalidation:
For a mutation, find elements possibly affected by any :has()
pseudo state change, and schedule invalidation on the elements.
After the invalidation is scheduled, the rest of the steps are the same as the traditional invalidation steps.
I've answered each item. Hope those help understanding how Chrome works.
- When styling an element, you record whether any :has() selector was tried against a given element (regardless of whether it matches), and store it in a variety of flags (some of them for some special pseudo-classes like :hover / :focus / etc).
Yes correct.
The 'affected-by' flags have been used in Chrome to optimize some invalidation cases by skipping most (not all) unnecessary invalidation:
:focus
, :hover
, :nth()
, ...)Similar to the above, we added some more 'affected-by' flags to optimize invalidation for :has()
pseudo-class state change.
- When something has changed that pseudo-class state (or that class or attribute or insertion or removal or so), you synchronously look at these flags and potentially (and unconditionally) restyle any node affected by a pseudo-class in :has(). This has some other sanity-checks like storing all classes and so that are used inside :has() to avoid work in a variety of common cases, is that right?
Yes right.
Using flags synchronously The key point of the traditional 'affected-by' flags approach is that, the flag indicates whether a pseudo state change possibly affects an element or not. StyleEngine checks the flag synchronously to avoid scheduling unnecessary invalidation.
This is same for :has()
. With the 'affected-by' flags for :has()
invalidation, we can determine whether an element is possibly affected by :has()
state change.
The difference is:
:has()
invalidation, the changed element and the marked element is not same (the marked element will be ancestor, previous sibling or ancestor previous sibling of the changed element).To handle the difference, the flags provides the path information from the changed element to the marked element so that the StyleEngine can reach to the marked element and check the flag synchronously.
The ancestors, previous sibling and ancestor's previous sibling of the changed element are the element to be checked whether it is possibly affected by any :has()
state change or not.
These are relatively small number of elements compared to the traditional invalidation scope (descendants, next siblings, next sibling subtrees), and we can skip checking some of those elements by using the 'affected-by' flags for :has()
invalidation.
Doing some sanity-checks by storing some identifiers such as class names or ids
Once an element was invalidated, and it is related to a style rule using :has()
, style recalculation will take $O(n)$ time where $n$ is the number of elements in the :has()
argument checking scope.
So, it would be better to skip unnecessary :has()
invalidation if possible.
Collecting some identifiers from selectors and filtering mutations with the identifiers is the Chrome invalidation approach, so we followed the approach to avoid unnecessary :has()
invalidation.
- Also, once :has() is used, you need to propagate the affected flags on DOM mutations potentially to all the inserted subtree, so that they remain correct.
Yes correct.
As I explained at above, the 'affected-by' for :has()
invalidation contains the path information from a changed element to the element possibly affected by any :has()
state change.
To make this work properly, Chrome propagate the 'affected-by' flags for inserted subtree ONLY when the subtree is inserted to any :has()
argument checking scope. (We can determine whether a subtree is inserted to a :has()
argument checking scope by checking the 'affected-by' flags of the around elements)
Firefox is the only browser that doesn't have this by default, why? You have to manually turn it on in the settings. I'm relatively new to CSS, how the hell did people use to make websites without this feature? And this isn't even something you can polyfill or transpile like nesting. It already exists and should have existed from the start, my website design relies on it. Firefox doesn't even tell you it doesn't work, it just completely ignores it.
Gentle ping @martinthomson/@tantek -
There has definitely been a steady stream of :has()
related bugs, and Safari and Chrome shipped it.
We are also in the prototyping stage for :has()
.
@dshin-moz, it was not clear from the feedback we got from @emilio that the outcome was indeed positive (as others have been) or whether we should instead be neutral. You can help by opening a pull request that adds this to the dashboard, maybe checking in with @emilio about what you think it should say.
I (as most people here) like the feature, but I'm still fairly concerned about performance of complex cases which don't hit the happy paths in browser engines.
I suspect sites will effectively rely on browsers having similar performance characteristics, which might end up being true and fine(-ish).
But even with that, there's the likely chance that web developers test only on powerful devices where this feature is "fast enough", and then users on lower powered devices suffer as a consequence. I suspect most people using the feature won't be aware of its performance footguns, specially longer term when this feature is not new anymore.
But it's way too easy to write a global :has
selector, or a couple :root:has(..)
and destroy the performance of a website.
I guess this is mostly my intuition tho, and we should implement it given other browsers do... But my position would be "positive with caveat", if that were to be a thing.
"positive with caveat"
... is totally a thing. That's pretty much everything we've ever marked positive :) Nothing is ideal.
It sounds like we should have a dashboard entry that tries to capture that detail. This is a significant feature and it would be good to memorialize the potential for footguns.
Something to consider is whether there ways for us to flag bad :has(..)
usage in a way that would help people avoid the worst performance characteristics. Two approaches I can see are static analysis (like finding deep trees with a :has
on top) or runtime measurement (like observing that restyling took longer or hit a particular code path that is longer).
Firefox is the only browser that doesn't have this by default, why? You have to manually turn it on in the settings. I'm relatively new to CSS, how the hell did people use to make websites without this feature? And this isn't even something you can polyfill or transpile like nesting. It already exists and should have existed from the start, my website design relies on it. Firefox doesn't even tell you it doesn't work, it just completely ignores it.
I'm not sure why this is receiving as many thumbs down's as it has. This selector is especially useful for beginner web engineers that may run into the increasingly large amount of articles/blog posts praising the versatility + the # of possible use-cases the :has()
selector has without realizing Firefox doesn't support it. I hope the Firefox team can understand that IMO the target audience for the :has()
selector is not primarily for veteran web engineers and its utility shines brightest for beginners. Why is this not a priority for the Firefox team to ship?
Firefox is the only browser that doesn't have this by default, why? You have to manually turn it on in the settings. I'm relatively new to CSS, how the hell did people use to make websites without this feature? And this isn't even something you can polyfill or transpile like nesting. It already exists and should have existed from the start, my website design relies on it. Firefox doesn't even tell you it doesn't work, it just completely ignores it.
I'm not sure why this is receiving as many thumbs down's as it has. This selector is especially useful for beginner web engineers that may run into the increasingly large amount of articles/blog posts praising the versatility + the # of possible use-cases the
:has()
selector has without realizing Firefox doesn't support it. I hope the Firefox team can understand that IMO the target audience for the:has()
selector is not primarily for veteran web engineers and its utility shines brightest for beginners. Why is this not a priority for the Firefox team to ship?
Well yeah, but I don't think that matters. Do veterans not use it? It's equally useful to everyone. No way I'm gonna manually write out extra code that makes everything unmaintainable, even if I was a veteran or I didn't know it was supported by Firefox or even now that I know it isn't supported. Do Mozilla seriously think everyone's just not gonna use it just because they don't support it? "Oh no, bad performance maybe". Well it's better than nothing. They definitely need it add it, I don't know what's going through their heads.
Mozilla engineers when you tell them there's a common web standard that they need to uphold: 🙉🙈
I (as most people here) like the feature, but I'm still fairly concerned about performance of complex cases which don't hit the happy paths in browser engines.
I suspect sites will effectively rely on browsers having similar performance characteristics, which might end up being true and fine(-ish).
But even with that, there's the likely chance that web developers test only on powerful devices where this feature is "fast enough", and then users on lower powered devices suffer as a consequence. I suspect most people using the feature won't be aware of its performance footguns, specially longer term when this feature is not new anymore.
But it's way too easy to write a global
:has
selector, or a couple:root:has(..)
and destroy the performance of a website.I guess this is mostly my intuition tho, and we should implement it given other browsers do... But my position would be "positive with caveat", if that were to be a thing.
You know you can emulate throttling, right? As for complex cases, just make the rules clear and simple, if it's slow you can just log a message and make it visible in the performance section of devtools. Maybe they could make a proposal to the web standard so that there are less compatibility and performance issues
They definitely need it add it, I don't know what's going through their heads.
You can see the progress in the tracking bug.
if it's slow you can just log a message and make it visible in the performance section of devtools.
If you had looked at Bugzilla, you'd know there's a bug for this.
Please don't +1 issues/bugs (see bradfitz/issue-tracker-behaviors), these comments add nothing to the discussion and your comments won't cause :has
to be magically implemented overnight. Almost every bug/issue tracker has some kind of voting system - use it.
You can see the progress in the tracking bug.
They have to track the progress of pressing Ctrl+C and Ctrl+V? While they're at it, they should also go copy and paste the entire V8 and CSS parser source code, so I don't have to deal with Mozilla's dumb shit. Subgrid since 2019, but not :has() in 2023?
If you had looked at Bugzilla...
- 🤡
- No need to be condescending.
- Why would I know about that?
- Who cares?
Some clarifications:
This repository is for standards positions, not for actual implementation. For that, there's bugzilla. The tracking bug for :has
is bug 418039, and implementation of :has
has been ongoing for a while (if you're curious, the biggest part of ongoing work is invalidation in bug 1792501.
My performance concerns with this feature are not blocking the implementation of it (heck, I'm the one reviewing it).
Finally, @coolCucumber-cat, please stop? There's no need to be a dick, and there's no need to comment nonsense. If you bothered investigating what the performance impact of the feature is, you'd realize that throttling has nothing to do with it / can't help debug it. Matching 3k DOM nodes every frame is something "fast" on a fast desktop/laptop, while on a mobile CPU, not so much. One of the biggest reasons :has()
is taking more time to implement in Firefox than in other engines is because we have parallel styling and invalidation, and :has()
changes those constraints (:has()
invalidates bottom-to-top, as opposed to every other CSS feature).
Finally, I'd also note that Firefox is open source, and if people want to see something implemented faster they can always send patches. I'd always be happy to mentor / review. In any case this repo is not the right place to discuss that.
This discussion should be about :has()
as a standard. Let's stop derailing that conversation please? :)
Request for Mozilla Position on an Emerging Web Specification
Other information
The :has() pseudo class is a selector that specifies elements which has at least one element that matches the relative selector passed as an argument.
Unlike other selectors, it gives a way to apply a style rule to preceding elements (preceding siblings / ancestors / preceding siblings of ancestors) of a certain elements.
This difference is attractive to web developers, but also it generates lots of concerns mainly about performance and complexity.
We thought that, clarifying those concerns would be helpful for the discussion. So we started to check feasibility on blink, and were able to get some meaningful and reasonable results from this step. Based on the results, we are going to move forward by prototyping.
You can get the details about the current status in here.