:has()-Selektor-Performance: Wann er wirklich teuer wird
AI generated
{ }
@
CSS · :has() · Performance
:has()-Selektor-Performance
Wann er wirklich teuer wird

Mit :has() kann CSS erstmals ein Element anhand seiner Nachfahren oder nachfolgenden Geschwister auswählen, eine Fähigkeit, auf die die Sprache jahrelang gewartet hat. Diese Mächtigkeit hat aber einen Preis: :has() zwingt den Browser, für jeden Kandidaten einen kompletten Teilbaum zu durchsuchen, statt nur einen einzelnen Knoten zu prüfen, was in großen DOM-Bäumen spürbar werden kann.

16 Min. Lesezeit :has() · Parent Selector Selector-Matching-Performance

1. Was :has() kann und warum es sich fundamental von anderen Selektoren unterscheidet

Der :has()-Selektor prüft, ob ein Element mindestens einen Nachfahren oder ein nachfolgendes Geschwister besitzt, der oder das einem übergebenen Selektor entspricht, und wählt in diesem Fall das ursprüngliche Element selbst aus. Ein Ausdruck wie .card:has(img) trifft also auf jede .card, die irgendwo in ihrem Nachfahren-Baum ein img-Element enthält, unabhängig davon, wie tief dieses Bild verschachtelt ist.

Fast jeder andere CSS-Selektor prüft eine feste, im Voraus bekannte Beziehung: ein Element selbst, ein direktes Kind, einen direkten Nachfolger. :has() dagegen prüft eine offene Frage über einen ganzen, potenziell beliebig tiefen Teilbaum hinweg, und genau diese strukturelle Offenheit ist die Wurzel seiner höheren Matching-Kosten im Vergleich zu klassischen Selektoren.

2. Warum :has() beim Style-Matching mehr Arbeit erzeugt

Normale CSS-Selektoren wertet der Browser von rechts nach links aus und muss dabei nur entlang der Vorfahren-Kette eines einzelnen Kandidaten nach oben schauen, ein Pfad mit klar begrenzter, meist geringer Länge. Bei :has() muss der Browser dagegen für jeden Kandidaten, der als Basis-Element in Frage kommt, dessen kompletten Nachfahren-Teilbaum durchsuchen, um festzustellen, ob irgendwo darin der :has()-Ausdruck erfüllt ist, ein potenziell sehr viel größerer und strukturell unbegrenzter Suchraum.

Browser-Engines wie Blink und WebKit begegnen diesem Mehraufwand mit internen Optimierungen, etwa indem sie beim Ändern eines Nachfahren-Elements gezielt nur die tatsächlich betroffenen Vorfahren neu bewerten, statt bei jeder DOM-Änderung den kompletten Baum erneut zu durchsuchen. Diese Optimierungen reduzieren die Kosten erheblich, heben sie aber nicht vollständig auf: Die grundsätzliche algorithmische Komplexität bleibt höher als bei einem Selektor, der nur einen einzelnen, festen Pfad prüft.

3. Typische :has()-Anwendungsfälle und ihr jeweiliges Kostenprofil

Ein häufiger, in der Praxis unproblematischer Anwendungsfall ist ein Formularfeld-Container, der abhängig vom Validierungszustand eines enthaltenen Eingabefelds gestylt wird, etwa .field:has(input:invalid). Da Formulare in der Regel überschaubar viele Felder enthalten und die Verschachtelungstiefe zwischen Container und Eingabefeld gering bleibt, ist der zu durchsuchende Teilbaum klein, und der Mehraufwand gegenüber einem klassischen Selektor bleibt praktisch nicht messbar.

Deutlich teurer wird ein Ausdruck wie body:has(.modal-open) nicht durch die Selektor-Struktur selbst, sondern durch die Größe des betroffenen Teilbaums: Das body-Element steht an der Wurzel des kompletten Dokuments, wodurch der Browser potenziell den gesamten DOM-Baum nach einem passenden .modal-open-Element durchsuchen muss. Bei einer sehr großen, verschachtelten Anwendung mit tausenden Elementen kann dieser Suchraum signifikant größer ausfallen als bei einem lokal begrenzten Formularfeld-Beispiel.


/* Guenstig: kleiner, flacher Suchraum innerhalb eines Formularfelds */
.field:has(input:invalid) {
  border-color: #dc2626;
}

/* Potenziell teuer: body als Wurzel-Element zwingt den Browser,
   im schlimmsten Fall den kompletten DOM-Baum zu durchsuchen */
body:has(.modal-open) {
  overflow: hidden;
}

4. Der entscheidende Faktor: DOM-Größe und Änderungsfrequenz

Die reine Präsenz von :has() im Stylesheet kostet nichts, solange sich der betroffene Teilbaum nicht ändert. Teuer wird es erst, wenn sich Elemente innerhalb des überwachten Teilbaums häufig ändern, etwa durch dynamisches Hinzufügen und Entfernen von Klassen bei jeder Nutzerinteraktion, weil der Browser bei jeder solchen Änderung erneut prüfen muss, ob sich am Ergebnis des :has()-Ausdrucks etwas verändert hat.

Besonders kritisch wird die Kombination aus großem DOM und hoher Änderungsfrequenz: Eine lange, virtualisierte Liste mit tausenden Einträgen, bei der jeder Tastendruck in einem Filterfeld Klassen an vielen Listeneinträgen gleichzeitig ändert, und ein :has()-Selektor, der auf einen dieser Zustände reagiert, kann in der Summe zu spürbaren Verzögerungen führen, während dieselbe Struktur bei einer statischen Seite mit wenigen hundert Elementen unproblematisch bleibt.

5. Messmethoden: Style-Recalculation-Kosten von :has() sichtbar machen

Der Chrome-DevTools-Performance-Tab zeigt unter Recalculate Style die aufgewendete Zeit für Style-Neuberechnungen an. Ein gezielter A/B-Vergleich, bei dem dieselbe Interaktion einmal mit einem :has()-Selektor und einmal mit einer funktional äquivalenten, aber JavaScript-basierten Klassensteuerung durchgeführt wird, macht den tatsächlichen Mehraufwand auf der konkreten Seite sichtbar, statt sich auf allgemeine Aussagen zu verlassen.

Zusätzlich hilft ein synthetischer Stresstest mit künstlich vergrößertem DOM: Dieselbe Komponente einmal in einem Dokument mit hundert Elementen und einmal in einem mit zehntausend Elementen rendern und dabei die Style-Recalculation-Zeit bei identischer Interaktion vergleichen. Skaliert die Kostenkurve deutlich überproportional zur DOM-Größe, ist das ein klares Signal, den betroffenen :has()-Selektor gezielt zu überarbeiten.


/* Eingrenzung des Suchraums statt globaler body-Selektor */
.app-shell:has(.modal-open) {
  overflow: hidden;
}

/* Noch enger gefasst: nur der direkte Layout-Wrapper */
.layout-root:has(> .modal-open) {
  overflow: hidden;
}

6. Optimierungsstrategien: den Suchraum von :has() aktiv begrenzen

Die wirksamste Optimierung ist, den :has()-Ausdruck so nah wie möglich an der tatsächlich relevanten Struktur zu verankern, statt ihn auf ein sehr weit oben stehendes Element wie body oder html anzuwenden. Ein spezifischerer Basis-Selektor wie .app-shell:has(.modal-open) statt body:has(.modal-open) reduziert den zu durchsuchenden Teilbaum erheblich, sofern .app-shell ohnehin schon näher an der tatsächlich betroffenen Struktur sitzt.

Zusätzlich hilft ein direkter Kind-Kombinator innerhalb des :has()-Arguments, etwa :has(> .item) statt :has(.item), weil der Browser dann nur die direkten Kinder statt des kompletten, beliebig tiefen Nachfahren-Baums prüfen muss. Wo die Anwendungslogik es zulässt, ist diese Einschränkung auf direkte Kinder oft die einfachste und wirksamste Optimierung, ohne den eigentlichen Anwendungsfall des Selektors zu verändern.

7. Wann eine JavaScript-Alternative sinnvoller ist als :has()

Bei sehr großen, hochdynamischen DOM-Bäumen, etwa Datentabellen mit zehntausenden Zeilen, bei denen ein :has()-Ausdruck bei jeder Nutzerinteraktion neu ausgewertet werden müsste, kann eine gezielte JavaScript-Lösung, die eine Zustandsklasse einmalig auf das relevante Ziel-Element setzt, günstiger sein als der laufende :has()-Matching-Aufwand über den gesamten Baum. Der JavaScript-Ansatz verlagert die Kosten von einer wiederholten Selektor-Auswertung auf eine einmalige, gezielte DOM-Manipulation.

Für die überwiegende Mehrheit der Anwendungsfälle, insbesondere Formulare, einzelne Karten-Komponenten und lokal begrenzte UI-Zustände, bleibt :has() jedoch die deutlich wartungsärmere und meist auch performantere Lösung, weil es keine zusätzliche JavaScript-Logik, keine Event-Listener und keine manuelle Synchronisation von CSS-Klassen mit Anwendungszustand benötigt.

8. Browser-Unterstützung und progressive Absicherung mit @supports selector()

Alle aktuellen Versionen von Chrome, Edge, Firefox und Safari unterstützen :has() inzwischen vollständig, sodass die reine Verfügbarkeit für die meisten Projekte kein Hindernis mehr darstellt. Für Projekte, die noch ältere Browser-Versionen unterstützen müssen, lohnt sich dennoch eine Absicherung, weil ein Browser ohne :has()-Unterstützung die betroffene Regel komplett ignoriert, statt auf ein alternatives Verhalten zurückzufallen.

Die Funktion @supports selector(:has(a)) prüft gezielt, ob der Browser den :has()-Selektor selbst unterstützt, getrennt von der allgemeinen @supports-Prüfung für Eigenschaften und Werte. So lässt sich ein alternativer, meist JavaScript-basierter Pfad ausschließlich für Browser aktivieren, die :has() tatsächlich nicht kennen, ohne moderne Browser mit unnötigem zusätzlichem Code zu belasten.


/* Basisverhalten ohne :has(), funktioniert ueberall */
.field {
  border-color: #cbd5e1;
}

/* Erweiterung nur fuer Browser, die :has() als Selektor unterstuetzen */
@supports selector(:has(a)) {
  .field:has(input:invalid) {
    border-color: #dc2626;
  }
}

9. :has() im Vergleich zu klassischen Selektoren und JavaScript-Klassensteuerung

Die folgende Übersicht ordnet die drei gängigen Ansätze nach Matching-Aufwand, Wartbarkeit und dem Kontext, in dem sie sich jeweils am besten eignen.

Ansatz Matching-Aufwand Wartbarkeit Empfohlener Kontext
Klassischer Selektor Konstant, unabhängig von der DOM-Größe Hoch, keine besonderen Regeln nötig Alle Standardfälle ohne Nachfahren-Bedingung
:has() mit engem Basis-Selektor Gering bis moderat, abhängig vom Teilbaum Hoch, kein zusätzlicher JavaScript-Code Formularfelder, Karten, lokale UI-Zustände
:has() mit body oder html als Basis Potenziell hoch bei großem DOM Mittel, funktioniert aber ohne JS Kleine bis mittlere Seiten, seltene Änderungen
JavaScript-Klassensteuerung Einmalig bei der Änderung, kein laufendes Matching Niedriger, zusätzlicher Code und Event-Handling nötig Sehr große, hochdynamische DOM-Bäume

Mironsoft

Modernes CSS, Layout-Architektur und Rendering-Performance

CSS, das wartbar bleibt statt mit jeder Änderung zu brechen?

Wir prüfen bestehende Stylesheets auf Spezifitäts-Chaos und Layout-Thrashing und bauen daraus eine CSS-Architektur mit Cascade Layers, Custom Properties und modernen Layout-Primitiven, die auch nach dem zehnten Feature noch verständlich ist.

CSS-Audit

Spezifität, Cascade-Konflikte und ungenutzte Selektoren systematisch aufdecken.

Architektur-Refactoring

Cascade Layers, Custom Properties und Design Tokens sauber einführen.

Performance-Tuning

Layout-Thrashing, teure Selektoren und Rendering-Engpässe gezielt beheben.

10. Zusammenfassung

:has()-Performance: Das Wichtigste auf einen Blick

Grundprinzip

:has() prüft einen ganzen Nachfahren-Teilbaum statt eines einzelnen festen Pfads, was strukturell mehr Matching-Aufwand erzeugt.

Entscheidender Faktor

Nicht die reine Präsenz von :has() ist teuer, sondern die Kombination aus großem, betroffenem Teilbaum und häufigen Änderungen darin.

Optimierung

Ein enger Basis-Selektor statt body oder html sowie direkte Kind-Kombinatoren im :has()-Argument reduzieren den Suchraum spürbar.

Messung

Der Recalculate-Style-Wert im Chrome-DevTools-Performance-Tab macht den tatsächlichen Mehraufwand auf der konkreten Seite sichtbar.

11. FAQ: :has()-Performance: Das Wichtigste auf einen Blick

1Warum ist :has() grundsätzlich teurer als ein klassischer Selektor?
Weil ein klassischer Selektor nur einen einzelnen, festen Pfad entlang der Vorfahren-Kette prüft, während :has() einen kompletten, potenziell beliebig tiefen Nachfahren-Teilbaum durchsuchen muss, um festzustellen, ob die Bedingung irgendwo darin erfüllt ist.
2Ist jeder :has()-Ausdruck automatisch ein Performance-Problem?
Nein. Bei kleinen, lokal begrenzten Teilbäumen wie einem einzelnen Formularfeld bleibt der Mehraufwand praktisch nicht messbar. Kritisch wird es erst bei großem, häufig verändertem DOM.
3Warum ist body:has(...) teurer als .field:has(...)?
Weil body an der Wurzel des kompletten Dokuments steht und der Browser im schlimmsten Fall den gesamten DOM-Baum durchsuchen muss, während .field nur einen kleinen, klar begrenzten Teilbaum umfasst.
4Wie kann ich :has() gezielt optimieren?
Den Basis-Selektor so nah wie möglich an der relevanten Struktur verankern statt body oder html zu verwenden, und wo möglich einen direkten Kind-Kombinator wie :has(> .item) statt eines offenen Nachfahren-Selektors nutzen.
5Wann sollte ich :has() durch JavaScript ersetzen?
Bei sehr großen, hochdynamischen DOM-Bäumen, etwa Datentabellen mit zehntausenden Zeilen, bei denen der :has()-Ausdruck bei jeder Interaktion neu ausgewertet werden müsste, kann eine gezielte JavaScript-Klassensteuerung günstiger sein.
6Optimieren Browser-Engines :has() intern?
Ja, Engines wie Blink und WebKit bewerten bei einer Änderung gezielt nur die tatsächlich betroffenen Vorfahren neu statt den kompletten Baum. Das reduziert die Kosten erheblich, hebt sie aber nicht vollständig auf.
7Wie messe ich die tatsächlichen Kosten von :has() auf meiner Seite?
Mit dem Recalculate-Style-Wert im Performance-Tab der Chrome DevTools, idealerweise im direkten A/B-Vergleich zwischen einer :has()-Lösung und einer funktional gleichwertigen JavaScript-Alternative.
8Macht die reine Anzahl der :has()-Selektoren im Stylesheet einen Unterschied?
Weniger als der jeweils betroffene Teilbaum und dessen Änderungsfrequenz. Zehn eng gefasste :has()-Selektoren auf kleinen Teilbäumen sind meist günstiger als ein einzelner, weit gefasster body:has()-Ausdruck.
9Ist ein direkter Kind-Kombinator innerhalb von :has() immer möglich?
Nur wenn die Anwendungslogik tatsächlich einen direkten Kind-Bezug erlaubt. Wird die Bedingung von einem tiefer verschachtelten Element erfüllt, ist ein offener Nachfahren-Selektor notwendig, was den größeren Suchraum in Kauf nimmt.
10Lohnt sich vorbeugende Optimierung ohne gemessenes Problem?
Meist nicht. Ein enger Basis-Selektor statt body ist eine gute Standardgewohnheit, aber tiefergehende Optimierung wie das Umschreiben auf JavaScript sollte erst nach einer tatsächlichen Messung erfolgen, nicht auf bloßen Verdacht.