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.
Inhaltsverzeichnis
- 1. Was :has() kann und warum es sich fundamental von anderen Selektoren unterscheidet
- 2. Warum :has() beim Style-Matching mehr Arbeit erzeugt
- 3. Typische :has()-Anwendungsfälle und ihr jeweiliges Kostenprofil
- 4. Der entscheidende Faktor: DOM-Größe und Änderungsfrequenz
- 5. Messmethoden: Style-Recalculation-Kosten von :has() sichtbar machen
- 6. Optimierungsstrategien: den Suchraum von :has() aktiv begrenzen
- 7. Wann eine JavaScript-Alternative sinnvoller ist als :has()
- 8. Browser-Unterstützung und progressive Absicherung mit @supports selector()
- 9. :has() im Vergleich zu klassischen Selektoren und JavaScript-Klassensteuerung
- 10. Zusammenfassung
- 11. FAQ
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.