Ein :has()-Selektor war 70 % der TBT: 369 → 108 ms.
Die Blockierzeit der Startseite stieg in einer Runde auf das Sechsfache; schuld waren nicht die neuen Szenen oder die Animationsbibliothek, sondern zehn CSS-Regeln, die auf dieser Seite nie passten.
Nach einer Entwicklungsrunde war die Blockierzeit der Startseite von 61 Millisekunden auf 369 gestiegen. Der erste Verdächtige war der übliche, JavaScript: in jener Runde waren zwei neue Szenen dazugekommen, und die Animationsbibliothek erzeugte lange Aufgaben. Die Messung sagte etwas anderes — teuer war nicht das Skript, sondern das Dokument, das es berührte.
Hypothese
Als wir die Browser-Spur in Skript, Stil und Layout zerlegten, wurde das Gewicht sichtbar: die gesamte Stilneuberechnung war von 423 Millisekunden auf 1.392 gestiegen. Das Dokument war tatsächlich gewachsen, von 671 auf 1.733 Elemente, aber darum ging es nicht: jedes Bild berechnete das GESAMTE Dokument neu. Das war die Hypothese — ein Selektor hängt an der Wurzel und macht bei jeder kleinen Änderung den ganzen Baum ungültig.
Methode
Zwei Bäume liefen nebeneinander: der Live-Stand von vier Tagen zuvor in einer temporären Arbeitskopie, der aktuelle Baum daneben, beide im selben Mobilprofil (412 × 823, Pixelverhältnis 1,75, Prozessor vierfach verlangsamt). Danach wurde die Stildatei im Flug über einen Anfrage-Abfänger umgeschrieben — Regelfamilien wurden einzeln entfernt, ohne dass sich auf der Platte ein Byte änderte, und jedes Mal wurde der Median der Blockierzeit gelesen. Auch die Ungültigkeitsspur des Browsers war eingeschaltet; sie schreibt Zeile für Zeile, welches Element von welchem Selektor betroffen war.
Datum und Umgebung
- Datum → 2026-09-21 · lokales A/B · zwei Bäume
- Profil → 412 × 823 · DPR 1,75 · CPU 4× langsamer
- Maß → TBT-Median · Stilneuberechnung · Elementzahl
- Spur → devtools.timeline.invalidationTracking
Ergebnis: der Bisect
Das familienweise Entfernen ließ den Schuldigen allein stehen. Ohne sämtliche :has()-Selektoren fiel die Blockierzeit von 369 Millisekunden auf 104; wurden nur jene mit html oder body als Subjekt entfernt, auf 115. Einzeln geprüft sammelte sich fast das gesamte Gewicht in einer einzigen Familie.
- wie ausgeliefert → TBT 369 ms
- alle :has() entfernt → 104 ms
- nur html/body-Subjekte → 115 ms
- :has(.menu … → 431 ms
- :has(main.oda) … → 430 ms
- :has(.sayfa__ … → 112 ms
Der Schuldige: zehn Regeln, die nie passten
Übrig blieben zehn Regeln der Basis-Stildatei, alle von derselben Form: ein :has() mit html als Subjekt, dahinter eine breite rechte Seite. Wir haben auch gemessen, dass diese Regeln auf der Startseite gar nicht passten — keine einzige malte dort etwas. Und trotzdem machte jeder Tween, jede Motor-Einrichtung das gesamte Dokument ungültig. Denn der Browser kann die Frage „könnte diese Änderung das Ergebnis jenes :has() an der Wurzel verändert haben?“ nur beantworten, indem er den Teilbaum neu berechnet. Die Spur sagte es beim Namen: 350 und 347 Einträge „von :has() betroffen“ auf html und body, mit dem Vermerk „Ungültigkeitsmenge macht Teilbaum ungültig“.
Die Lösung: die Frage in den Build verschieben
Die Frage blieb dieselbe, der Fragende wechselte. Welchen Wirt eine Seite trägt, ist eine Struktur, die sich nach dem Laden nicht ändert — sie ist also schon zur Bauzeit bekannt. Der Generator sieht sich das fertige HTML an und setzt eine Marke auf das Wurzelelement; die Stildatei fragt jetzt diese Marke. Die Spezifität blieb exakt gleich, keine Regelreihenfolge verschob sich; der Unterschied der berechneten Stile war null, über zwölf Routen, zwei Breiten und mehr als fünfzehntausend Elemente.
- lokale TBT → 369 → 97–108 ms (5 Läufe)
- Stilneuberechnung → 1.415 → 783 ms
- Stildifferenz → 0 (12 Routen × 2 Breiten)
- Live-TBT-Median → 293 → 83 / 86 ms
Der Wächter
Der Befund wurde an ein Tor gebunden, denn eine nur als Kommentar festgehaltene Entscheidung wird in der nächsten Runde still gebrochen. Das Tor fragt dreierlei: stimmen Marke und Wirt auf der Platte in beide Richtungen überein, ist jenes Muster in der gebauten Stildatei weiterhin abwesend, und wie oft wird der Teilbaum der Wurzel beim Laden zweier Routen ungültig. Dass das Tor fallen kann, wurde durch Wiedereinsetzen des Fehlers geprüft: mit den alten Regeln steigt der Zähler von zwei auf neunundvierzig. Ein Wächter, der nicht fallen kann, ist nicht grün, sondern nur still.
Grenzen
Diese Zahlen gehören zu dieser Maschine und diesem Dokument; auf einer anderen Seite fällt das Verhältnis anders aus. Die Lehre lautet auch nicht „:has() ist langsam“ — am richtigen Ort eingesetzt ist der Selektor günstig. Die Lehre lautet: ein :has() mit html oder body als Subjekt macht bei jeder Änderung das ganze Dokument ungültig, selbst auf einer Seite, auf der es nie passt. Eine Struktur, die sich nach dem Laden nicht ändert, markiert man zur Bauzeit, statt sie zur Laufzeit mit einem Selektor zu erfragen.
Ein Selektor kostet dort, wo er sucht, nicht dort, wo er passt.