mozilla / standards-positions

https://mozilla.github.io/standards-positions/
Mozilla Public License 2.0
633 stars 69 forks source link

:has() pseudo class #528

Closed byung-woo closed 6 months ago

byung-woo commented 3 years ago

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.

emilio commented 3 years 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...

byung-woo commented 3 years ago

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.

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

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?

bkardell commented 2 years ago

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

bgrins commented 2 years ago

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.

kdashg commented 2 years ago

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.

emilio commented 2 years ago

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:

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.

byung-woo commented 2 years ago

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:

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:

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)

coolCucumber-cat commented 1 year ago

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.

dshin-moz commented 1 year ago

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().

martinthomson commented 1 year ago

@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.

emilio commented 1 year ago

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.

martinthomson commented 1 year ago

"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).

TuckerMassad commented 1 year ago

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?

coolCucumber-cat commented 1 year ago

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: 🙉🙈

coolCucumber-cat commented 1 year ago

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

Nerixyz commented 1 year ago

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.

coolCucumber-cat commented 1 year ago

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...

  1. 🤡
  2. No need to be condescending.
  3. Why would I know about that?
  4. Who cares?
emilio commented 1 year ago

Some clarifications:

This discussion should be about :has() as a standard. Let's stop derailing that conversation please? :)