Haben Sie bemerkt, dass die CSS-Eigenschaften auf dem Tab Styles in den Chrome DevTools in letzter Zeit etwas ausgefeilter aussehen? Diese Updates, die zwischen Chrome 121 und 128 eingeführt wurden, sind das Ergebnis einer erheblichen Verbesserung bei der Analyse und Darstellung von CSS-Werten. In diesem Artikel gehen wir auf die technischen Details dieser Umstellung ein: von einem System zum Abgleichen von regulären Ausdrücken zu einem robusteren Parser.
Vergleichen wir die aktuellen DevTools mit der vorherigen Version:
Ein ziemlicher Unterschied, oder? Hier sind die wichtigsten Verbesserungen:
color-mix
: Eine praktische Vorschau, die die beiden Farbargumente in der Funktioncolor-mix
visuell darstellt.pink
: Eine anklickbare Farbvorschau für die benannte Farbepink
. Klicken Sie darauf, um eine Farbauswahl zu öffnen und die Farbe anzupassen.var(--undefined, [fallback value])
. Verbesserte Verarbeitung von nicht definierten Variablen: Die nicht definierte Variable wird ausgegraut und der aktive Fallback-Wert (in diesem Fall eine HSL-Farbe) wird mit einer anklickbaren Farbvorschau angezeigt.hsl(…)
: Eine weitere anklickbare Farbvorschau für die Farbfunktionhsl
, die schnellen Zugriff auf die Farbauswahl bietet.177deg
: Eine anklickbare Winkeluhr, mit der Sie den Winkelwert interaktiv ziehen und ändern können.var(--saturation, …)
: Ein klickbarer Link zur Definition der benutzerdefinierten Property, über den Sie ganz einfach zur entsprechenden Deklaration gelangen.
Der Unterschied ist auffällig. Dazu mussten wir DevTools beibringen, CSS-Attributwerte viel besser zu verstehen als bisher.
Waren diese Vorschauen nicht schon verfügbar?
Diese Vorschausymbole sind Ihnen vielleicht schon bekannt, wurden aber nicht immer einheitlich angezeigt, insbesondere bei komplexer CSS-Syntax wie im Beispiel oben. Selbst wenn sie funktionierten, waren oft erhebliche Anstrengungen erforderlich, um sie richtig zum Laufen zu bringen.
Das liegt daran, dass das System zur Analyse von Werten seit den ersten Tagen von DevTools organisch gewachsen ist. Sie konnte jedoch nicht mit den neuesten, beeindruckenden Funktionen von CSS und der damit verbundenen Komplexitätssteigerung der Sprache Schritt halten. Das System musste komplett neu gestaltet werden, um mit der Entwicklung Schritt zu halten. Genau das haben wir getan.
So werden CSS-Eigenschaftswerte verarbeitet
In DevTools wird das Rendern und Dekorieren von Eigenschaftsdeklarationen auf dem Tab Styles in zwei Phasen unterteilt:
- Strukturanalyse. In dieser ersten Phase wird die Property-Deklaration analysiert, um die zugrunde liegenden Komponenten und ihre Beziehungen zu identifizieren. In der Deklaration
border: 1px solid red
würde beispielsweise1px
als Länge,solid
als String undred
als Farbe erkannt. - Rendering Aufbauend auf der strukturellen Analyse werden diese Komponenten in der Rendering-Phase in eine HTML-Darstellung umgewandelt. So wird der angezeigte Text der Unterkunft durch interaktive Elemente und visuelle Hinweise ergänzt. Der Farbwert
red
wird beispielsweise mit einem anklickbaren Farbsymbol gerendert. Wenn Sie darauf klicken, wird eine Farbauswahl angezeigt, mit der Sie die Farbe ganz einfach ändern können.
Reguläre Ausdrücke
Bisher haben wir reguläre Ausdrücke (Regex) verwendet, um die Attributwerte für die strukturelle Analyse zu analysieren. Wir haben eine Liste von regulären Ausdrücken erstellt, um die Teile der Property-Werte abzugleichen, die wir dekorieren wollten. Es gab beispielsweise Ausdrücke, die CSS-Farben, -Längen, -Winkel und komplexere Unterausdrücke wie var
-Funktionsaufrufe abglichen haben. Wir haben den Text von links nach rechts durchgescannt, um eine Werteanalyse durchzuführen, und dabei ständig nach dem ersten Ausdruck aus der Liste gesucht, der mit dem nächsten Textabschnitt übereinstimmt.
Das funktionierte zwar meistens gut, aber die Anzahl der Fälle, in denen es nicht funktionierte, stieg immer weiter an. Im Laufe der Jahre haben wir eine große Anzahl von Fehlermeldungen erhalten, bei denen die Übereinstimmung nicht ganz richtig war. Bei der Behebung dieser Probleme – einige waren einfach, andere recht kompliziert – mussten wir unseren Ansatz überdenken, um unsere technischen Schulden in Schach zu halten. Sehen wir uns einige dieser Probleme an.
Übereinstimmung color-mix()
Der reguläre Ausdruck, den wir für die color-mix()
-Funktion verwendet haben, sah so aus:
/color-mix\(.*,\s*(?<firstColor>.+)\s*,\s*(?<secondColor>.+)\s*\)/g
Die Syntax lautet:
color-mix(<color-interpolation-method>, [<color> && <percentage [0,100]>?]#{2})
Führen Sie das folgende Beispiel aus, um die Übereinstimmungen zu visualisieren.
const re = /color-mix\(.*,\s*(?<firstColor>.+)\s*,\s*(?<secondColor>.+)\s*\)/g;
// it works - simpler example
const simpler = re.exec('color-mix(in srgb, pink, hsl(127deg 100% 50%))');
console.table(simpler.groups);
re.exec('');
// it doesn't work - complex example
const complex = re.exec('color-mix(in srgb, pink, var(--undefined, hsl(127deg var(--saturation, 100%) 50%)))');
console.table(complex.groups);
Das einfachere Beispiel funktioniert gut. Im komplexeren Beispiel ist <firstColor>
mit hsl(177deg var(--saturation
und <secondColor>
mit 100%) 50%))
übereinstimmend, was völlig sinnlos ist.
Wir wussten, dass das ein Problem ist. CSS ist schließlich keine reguläre formale Sprache. Daher haben wir bereits eine besondere Verarbeitung für komplexere Funktionsargumente wie var
-Funktionen implementiert. Wie Sie jedoch im ersten Screenshot sehen, funktionierte das nicht in allen Fällen.
Übereinstimmung tan()
Einer der amüsanteren gemeldeten Fehler betraf die trigonometrische Funktion tan()
. Der reguläre Ausdruck, den wir für den Abgleich von Farben verwendet haben, enthielt einen Unterausdruck \b[a-zA-Z]+\b(?!-)
für den Abgleich mit benannten Farben wie dem Keyword red
. Dann haben wir geprüft, ob der übereinstimmende Teil tatsächlich eine benannte Farbe ist. Und siehe da: tan
ist auch eine benannte Farbe. Wir haben also tan()
-Ausdrücke fälschlicherweise als Farben interpretiert.
Übereinstimmung var()
Sehen wir uns ein weiteres Beispiel an: var()
-Funktionen mit einem Fallback, der andere var()
-Referenzen enthält: var(--non-existent, var(--margin-vertical))
.
Unser regulärer Ausdruck für var()
würde mit diesem Wert übereinstimmen. Die Übereinstimmung würde jedoch bei der ersten schließenden Klammer enden. Der obige Text wird also als var(--non-existent, var(--margin-vertical)
erkannt. Das ist eine klassische Einschränkung der regulären Ausdrucksabgleiche. Sprachen, die übereinstimmende Klammern erfordern, sind grundsätzlich nicht regulär.
Umstellung auf einen CSS-Parser
Wenn die Textanalyse mit regulären Ausdrücken nicht mehr funktioniert, weil die analysierte Sprache nicht regulär ist, gibt es einen kanonischen nächsten Schritt: Verwenden Sie einen Parser für eine Grammatik höheren Typs. Bei CSS bedeutet das einen Parser für kontextfreie Sprachen. Tatsächlich gab es bereits ein solches Parsersystem in der DevTools-Codebasis: der Lezer von CodeMirror, der beispielsweise die Grundlage für die Syntaxhervorhebung in CodeMirror bildet, dem Editor im Bereich Quellen. Mit dem CSS-Parser von Lezer konnten wir (nicht abstrakte) Syntaxbäume für CSS-Regeln erstellen und sofort verwenden. Sieg.
Allerdings war es nicht möglich, direkt vom regexbasierten Abgleich zum parserbasierten Abgleich zu migrieren, da die beiden Ansätze in entgegengesetzter Richtung funktionieren. Beim Abgleichen von Werten mit regulären Ausdrücken scannte DevTools die Eingabe von links nach rechts und versuchte wiederholt, die früheste Übereinstimmung aus einer sortierten Liste von Mustern zu finden. Bei einem Syntaxbaum würde das Abgleichen von unten nach oben beginnen, z. B. werden zuerst die Argumente eines Aufrufs analysiert, bevor versucht wird, den Funktionsaufruf abzugleichen. Stellen Sie sich das als Auswertung eines arithmetischen Ausdrucks vor, bei dem Sie zuerst Ausdrücke in Klammern, dann Multiplikationsoperatoren und dann Additionsoperatoren berücksichtigen würden. In diesem Fall entspricht die reguläre Ausdrucksabgleichung der Auswertung des arithmetischen Ausdrucks von links nach rechts. Wir wollten das gesamte Abgleichsystem nicht von Grund auf neu schreiben: Es gab 15 unterschiedliche Abgleich- und Renderer-Paare mit Tausenden von Codezeilen, was es unwahrscheinlich machte, dass wir es in einem einzigen Meilenstein veröffentlichen konnten.
Wir haben daher eine Lösung entwickelt, mit der wir inkrementelle Änderungen vornehmen konnten. Diese werden wir unten genauer beschreiben. Kurz gesagt: Wir haben den zweiphasigen Ansatz beibehalten, aber in der ersten Phase versuchen wir, Unterausdrücke von unten nach oben abzugleichen (wodurch der Regex-Ablauf unterbrochen wird) und in der zweiten Phase rendern wir von oben nach unten. In beiden Phasen konnten wir die vorhandenen regexbasierten Matcher und Renderer praktisch unverändert verwenden und so einzeln migrieren.
Phase 1: Bottom-Up-Abgleich
In der ersten Phase wird mehr oder weniger genau und ausschließlich das getan, was auf dem Deckel steht. Wir durchlaufen den Baum in der Reihenfolge von unten nach oben und versuchen, Unterausdrücke an jedem Syntaxbaumknoten abzugleichen, den wir besuchen. Um einen bestimmten Teilausdruck abzugleichen, kann ein Matcher wie im bestehenden System reguläre Ausdrücke verwenden. In Version 128 ist das in einigen Fällen noch der Fall, z. B. bei übereinstimmenden Längen. Alternativ kann ein Matcher die Struktur des untergeordneten Knotens analysieren, der am aktuellen Knoten beginnt. So können Syntaxfehler erkannt und gleichzeitig strukturelle Informationen erfasst werden.
Sehen wir uns das Beispiel für den Syntaxbaum oben an:
Für diesen Baum würden unsere Übereinstimmer in der folgenden Reihenfolge angewendet:
hsl(
177deg
var(--saturation, 100%) 50%)
: Zuerst sehen wir uns das erste Argument deshsl
-Funktionsaufrufs an, den Farbtonwinkel. Wir ordnen ihm einen Winkelabgleich zu, damit wir den Winkelwert mit dem Winkelsymbol versehen können.hsl(177deg
var(--saturation, 100%)
50%)
: Zweitens finden wir den Funktionsaufrufvar
mit einem Var-Matcher. Bei solchen Anrufen möchten wir hauptsächlich zwei Dinge tun:- Rufen Sie die Deklaration der Variablen auf und berechnen Sie ihren Wert. Fügen Sie dem Variablennamen einen Link und ein Popover hinzu, um eine Verbindung zu ihnen herzustellen.
- Wenn der berechnete Wert eine Farbe ist, versehen Sie den Aufruf mit einem Farbsymbol. Es gibt noch eine dritte Sache, aber dazu kommen wir später.
hsl(177deg var(--saturation, 100%) 50%)
: Als Nächstes gleichen wir den Aufrufausdruck für die Funktionhsl
ab, damit wir ihn mit dem Farbsymbol versehen können.
Neben der Suche nach Unterausdrücken, die wir dekorieren möchten, gibt es noch eine zweite Funktion, die wir im Rahmen des Abgleichs ausführen. In Schritt 2 haben wir gesagt, dass wir den berechneten Wert für einen Variablennamen abrufen. Wir gehen sogar noch einen Schritt weiter und leiten die Ergebnisse nach oben in den Stammbaum weiter. Und das gilt nicht nur für die Variable, sondern auch für den Fallback-Wert. Wenn ein var
-Funktionsknoten besucht wird, wurden seine untergeordneten Knoten bereits zuvor besucht. Daher sind die Ergebnisse aller var
-Funktionen, die im Fallback-Wert erscheinen könnten, bereits bekannt. So können wir var
-Funktionen ganz einfach und kostengünstig durch ihre Ergebnisse ersetzen. So lassen sich Fragen wie „Ist das Ergebnis dieses var
-Aufrufs eine Farbe?“ ganz einfach beantworten, wie wir es in Schritt 2 getan haben.
Phase 2: Top-Down-Rendering
In der zweiten Phase ändern wir die Richtung. Anhand der Abgleichsergebnisse aus Phase 1 rendern wir den Baum in HTML, indem wir ihn von oben nach unten durchgehen. Für jeden besuchten Knoten wird geprüft, ob eine Übereinstimmung vorliegt. Ist das der Fall, wird der entsprechende Renderer des Matchers aufgerufen. Durch einen Standardabgleich und ‑Renderer für Textknoten ist keine spezielle Verarbeitung für Knoten erforderlich, die nur Text enthalten (z. B. NumberLiteral
„50 %“). Renderer geben einfach HTML-Knoten aus, die in Kombination die Darstellung des Property-Werts einschließlich seiner Dekorationen ergeben.
Im Beispielbaum wird der Property-Wert in der folgenden Reihenfolge gerendert:
- Rufen Sie den Funktionsaufruf
hsl
auf. Es gibt eine Übereinstimmung. Rufen Sie den Renderer der Farbfunktion auf. Sie hat zwei Funktionen:- Der tatsächliche Farbwert wird mit dem On-the-fly-Ersetzungsmechanismus für alle
var
-Argumente berechnet und dann wird ein Farbsymbol gezeichnet. - Die untergeordneten Elemente von
CallExpression
werden rekursiv gerendert. Dadurch werden der Funktionsname, Klammern und Kommas automatisch gerendert, da es sich dabei nur um Text handelt.
- Der tatsächliche Farbwert wird mit dem On-the-fly-Ersetzungsmechanismus für alle
- Rufen Sie das erste Argument des
hsl
-Aufrufs auf. Es wurde eine Übereinstimmung gefunden. Rufen Sie daher den Winkel-Renderer auf, der das Winkelsymbol und den Text des Winkels zeichnet. - Rufen Sie das zweite Argument auf, also den
var
-Aufruf. Die Übereinstimmung wurde gefunden, sodass die Variable renderer aufgerufen wird. Die folgende Ausgabe wird ausgegeben:- Der Text
var(
am Anfang. - Der Variablenname wird entweder mit einem Link zur Definition der Variablen oder mit einer grauen Textfarbe versehen, um anzugeben, dass sie nicht definiert wurde. Außerdem wird der Variable ein Pop-up hinzugefügt, in dem Informationen zu ihrem Wert angezeigt werden.
- Das Komma und dann der Fallback-Wert werden rekursiv gerendert.
- Eine schließende Klammer.
- Der Text
- Rufe das letzte Argument des
hsl
-Aufrufs auf. Es gibt keine Übereinstimmung. Daher wird nur der Textinhalt ausgegeben.
Haben Sie bemerkt, dass in diesem Algorithmus ein Rendervorgang vollständig steuert, wie die untergeordneten Elemente eines übereinstimmenden Knotens gerendert werden? Das rekursive Rendern der untergeordneten Elemente ist proaktiv. Dieser Trick ermöglichte eine schrittweise Migration vom regulären Ausdruck-basierten Rendering zum syntaxbaumbasierten Rendering. Bei Knoten, die mit einem alten regulären Ausdruck abgeglichen werden, kann der entsprechende Renderer in seiner ursprünglichen Form verwendet werden. Im Syntaxbaum würde es für das Rendern des gesamten untergeordneten Baums verantwortlich sein und sein Ergebnis (ein HTML-Knoten) könnte nahtlos in den umgebenden Rendering-Prozess eingebunden werden. So hatten wir die Möglichkeit, Matcher und Renderer paarweise zu portieren und nacheinander auszutauschen.
Eine weitere coole Funktion von Renderern, die das Rendern der Kinder des übereinstimmenden Knotens steuern, ist, dass wir Abhängigkeiten zwischen den hinzugefügten Symbolen berücksichtigen können. Im obigen Beispiel hängt die Farbe, die von der Funktion hsl
erzeugt wird, offensichtlich vom Farbton ab. Das bedeutet, dass die Farbe, die durch das Farbsymbol angezeigt wird, vom Winkel abhängt, der durch das Winkelsymbol angezeigt wird. Wenn der Nutzer über dieses Symbol den Winkeleditor öffnet und den Winkel ändert, können wir die Farbe des Farbsymbols jetzt in Echtzeit aktualisieren:
Wie Sie im Beispiel oben sehen, verwenden wir diesen Mechanismus auch für andere Symbolpaare, z. B. für color-mix()
und seine beiden Farbkanäle oder var
-Funktionen, die eine Farbe aus dem Fallback zurückgeben.
Auswirkungen auf die Leistung
Bei der Untersuchung dieses Problems, um die Zuverlässigkeit zu verbessern und langjährige Probleme zu beheben, haben wir mit einer gewissen Leistungseinbuße gerechnet, da wir einen vollwertigen Parser verwenden. Für diesen Test haben wir einen Benchmark erstellt, mit dem etwa 3.500 Property-Deklarationen gerendert wurden. Dabei wurden sowohl die regex-basierte als auch die parserbasierte Version mit einer sechsfachen Drosselung auf einem M1-Rechner profiliert.
Wie erwartet, war der parsebasierte Ansatz in diesem Fall 27% langsamer als der reguläre Ausdruck. Der Regex-basierte Ansatz benötigte 11 Sekunden für das Rendern und der parserbasierte Ansatz 15 Sekunden.
Angesichts der Vorteile, die wir mit dem neuen Ansatz erzielen, haben wir uns entschieden, ihn weiterzuverfolgen.
Danksagungen
Unser besonderer Dank geht an Sofia Emelianova und Jecelyn Yeen für ihre wertvolle Hilfe bei der Bearbeitung dieses Beitrags.
Vorschaukanäle herunterladen
Verwenden Sie als Standard-Entwicklungsbrowser Chrome Canary, Chrome Dev oder Chrome Beta. Diese Vorabversionen bieten Ihnen Zugriff auf die neuesten DevTools-Funktionen, ermöglichen es Ihnen, innovative Webplattform-APIs zu testen, und helfen Ihnen, Probleme auf Ihrer Website zu finden, bevor Ihre Nutzer sie bemerken.
Chrome-Entwicklertools-Team kontaktieren
Mit den folgenden Optionen können Sie über neue Funktionen, Updates oder andere Themen im Zusammenhang mit den DevTools sprechen.
- Senden Sie uns Feedback und Funktionsanfragen unter crbug.com.
- Melden Sie ein DevTools-Problem über das Dreipunkt-Menü Weitere Optionen > Hilfe > DevTools-Problem melden.
- Tweeten Sie an @ChromeDevTools.
- Hinterlassen Sie Kommentare unter den YouTube-Videos zu den Neuigkeiten in den DevTools oder den YouTube-Videos mit Tipps zu den DevTools.