:has()
The :has()
pseudo class has been in and out of CSS draft
specifications in various forms since 1998. There is a clear and
repeatedly expressed desire for the ability to style a subject based on
qualities of its contents. Performance and complexity concerns have been
the most oft cited reasons for not prioritizing work in this area.
However, it is difficult for the working group to make important decisions
on what is and isn't possible for many use cases without practical
implementation experimentation. To this end,
Igalia, with funding from
eyeo has been giving this priority.
There is quite a lot more detailed information on designs and
implementation work as things develop, currently available in
an explainers repository. This page will attempt to hit highlights and offer/link to informative
tests. Note that these tests require Igalia's current
:has()
-supporting build for testing.
Based on a long history of discussions and the number of challenges at
hand, our implementation begins with very strict limits that are not in
the selectors draft, but which are reflective of previous spec attempts
and numerous discussions over the years as what was potentially most
implementable. These are no final limits on the proposal but rather a
starting point in order to focus the discussions. The limits we place on
:has()
initially are:
:has
In fact, we believe (even with some supporting data) these limits are not ultimately desirable and can be loosened significantly. Supporting many classes are pseudos in the arguments, we belive is fairly trivial, in fact. Some, however, like logical combination pseudoclasses introduce a lot of complexity and questions. These initial limits are intended to focus the discussion and set a practical bar that we can overcome or not: if we can not solve acceptably with these limits, that is itself informative..
There are a large number of ways we can look at performance - none of them is particularly perfect, but each offers different insights.
CSS selector performance specifically is affected by two separate "phases": Style invalidation, and style (re)calculation. As the DOM tree changes, the invalidation phase attempts to quickly determine what parts of the tree has styles which could be affected by a change. Afterward, the style (re)calculation phase determines what the new styles actually are. These phases are not directly exposed to authors for measurement, however, one way to isolate them is to perform modifications that would cause invalidation, but not recalculation.
It can be useful to compare how the performance of :has()
measures up in both of these phases against the highly optimized selectors that the
browser supports today.
This test
does just that. It isolates invalidation tests with two rules, one for .c .d
and one
for .b:has(.a)
. we compare the effects of using
addClass
and removeClass
on trees of
similar size and depth trees where a .c
or an
.a
class which wouldn't match the whole selector is
toggled, thus causing invalidation but not recalculation. Sample
results are shown below, in this case :has
invalidation
is faster than common invalidations.
A number of things could affect this measure in real use... Below are some of these variables and more information about how they scale to real use...
The invalidation time for :has()
in our
implementation scales pretty well with depth.
This test simply adds 10x depth to the potential invaliation set. The results, shown below, are hardly affected.
:has()
In real use, several :has()
rules may hinge upon
the same change. That is, given
.one:has(.b){...}
and
.two:has(.b){...}
- a change to
.b
invalidates two subjects.
This test
involves toggling classes inside of :has()
which
potentially invalidate N
subjects. The results increase linearly.
:has()
The subjects of a :has()
rules may be compound.
That is, a rule could match
.one.two.three:has(.b){...}
.
This test
involves toggling classes inside of :has()
which
potentially invalidate N
things that match complex subjects. The results show no
discernable difference for the number of compound selectors in a
subject.
:has()
The arguments to a :has()
pseudoclass may
also be compound. That is, in addition to the previous
test a rule could match .one:has(.b.c.d.e){...}
..
This test
involves toggling classes inside of :has()
which
potentially invalidate N things that match a subject
(potentially compound). The results show a comparative (but
slight in real terms) decrease in the perfomance as the number
of simple selectors inside and outside the
:has()
increase.
:has()
The arguments to a :has()
pseudoclass contain
complex arguements with many potential matches. That is, in addition to the previous
test a rule could match .one:has(.b .c .d .e){...}
and
classes could toggle any of .b, .c, .d,
or .e
.
This test
involves toggling classes inside of :has()
invalidate
non-subject elements. The results show a linear scale in the perfomance as the number
of simple selectors inside which are invalidaed increase.
One can compare this with a similar test of native selectors via this test, the result looks indiscernable.
This test
does something similar to the invalidation only test above, but here
involves recalculation. Sample results are shown below, in this case
:has
invalidation is slower than common invalidations.
Each of the above tests provides interesting, discrete data, but ultimately leave a lot to the imagination regarding what real world impacts these might have as authors employ them. For this, we provide a few tests with some common sorts of challenges as played out against a significantly large tree or bigger, and commentary...
Entire stylesheets are frequently "inserted" or "removed" from
active duty - (this could be through actual DOM insertion of new style
sheets, or enabling or
disabling existing ones, or - more commonly through MediaQueries).
Changing the stylesheets and rules in play comes with its own performance
challenges, so we'd like to know that such a change is not disruptive
in practice. The following tests attempts to give a realistic "whole" picture
compariative cost of invalidating, recalculating and actually painting
both an "average size tree (
average tree test
")" and a "significantly large tree
very large tree test"by enabling or disabling stylesheets
which contain :has
based rules, and stylesheets which do
not.
The output below shows the impact. On average size trees, there is no noticable impact (differences are within the margin of normal variance). Very large trees do see around a 2x change, however these are still fairly small and seem reasonably comfortable/unnoticable for a user.
Just about the most stressful test we could imagine, and
frequently cited in CSS Working Group when this feature has been
discussed historically was a static version of the HTML5 living
standard single page edition which incorporates some :has() rules
- some which would affect rendering as the page was loading and a
user was scrolling, and one rule that affects the entire body only
when the very last element is received.
This test sets up
just that. Overall, the actual user experience is not noticably
different than it is with the baseline version of the same
document without :has()
rules at all. Presumably this
has to do with the streaming nature of things and the fact that
things repeatedly yield to the parser.
Note: All of the tests here involve a "signficantly large tree" with some depth: Over 2k elements, which, based on HTTPArchive data and previous studies, this is more elements than nearly all common pages, so a nice number to choose. Outliers beyond this vary wildly in just how big they attempt to get. We did include one, the HTML5 Living standard single page edition which includes orders of magnitude more elements for stetched perspective.
:has