Mehr als reguläre Ausdrücke: Optimiertes Parsen von CSS-Werten in den Chrome-Entwicklertools

Philip Pfaffe
Ergün Erdogmus
Ergün Erdogmus

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 beim Parsen und Darstellen von CSS-Werten. In diesem Artikel werden die technischen Details dieser Transformation erläutert – der Wechsel von einem System zum Abgleich regulärer Ausdrücke zu einem robusteren Parser.

Vergleichen wir die aktuellen DevTools mit der vorherigen Version:

Oben: Es ist die aktuelle Version von Chrome, Unten: Chrome 121.

Ein ziemlicher Unterschied, oder? Hier sind die wichtigsten Verbesserungen:

  • color-mix: Eine praktische Vorschau, die die beiden Farbargumente in der Funktion color-mix visuell darstellt.
  • pink: Eine anklickbare Farbvorschau für die benannte Farbe pink. 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 Farbfunktion hsl, 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 Eigenschaft, über den Sie ganz einfach die entsprechende Deklaration aufrufen können.

Der Unterschied ist auffällig. Dazu mussten wir DevTools beibringen, CSS-Eigenschaftswerte 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 in Fällen, in denen sie funktionierten, war häufig ein erheblicher Aufwand erforderlich, um sie ordnungsgemäß zu funktionieren.

Der Grund dafür ist, dass das System zum Analysieren von Werten seit den ersten Tagen der Entwicklertools organisch gewachsen ist. Sie konnte jedoch nicht mit den neuesten, fantastischen 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:

  1. 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 wird beispielsweise 1px als Länge, solid als String und red als Farbe erkannt.
  2. 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

Zuvor haben wir uns mit regulären Ausdrücken (Regexes) beschäftigt, um die Attributwerte für die Strukturanalyse zu zerlegen. 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 gescannt, um eine Wertanalyse durchzuführen. Dabei wurde kontinuierlich nach dem ersten Ausdruck in der Liste gesucht, der mit dem nächsten Teil des Textes ü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, war:

/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);

Abgleichsergebnis für die Funktion „Farbmix“.

Das einfachere Beispiel funktioniert gut. Im komplexeren Beispiel ist die Übereinstimmung <firstColor> jedoch hsl(177deg var(--saturation und die Übereinstimmung <secondColor> ist 100%) 50%)), was völlig bedeutungslos ist.

Wir wussten, dass das ein Problem ist. CSS ist schließlich keine reguläre formale Sprache. Deshalb 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.

Ein Syntaxbaum für den Eigenschaftswert `hsl(177deg var(--saturation, 100%) 50%)`. Es ist eine vereinfachte Version des vom Lezer-Parser erzeugten Ergebnisses, bei dem rein syntaktische Knoten für Kommas und Klammern weggelassen werden.

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 verschiedene Abgleich- und Renderer-Paare mit Tausenden von Codezeilen, was es unwahrscheinlich machte, dass wir es in einem einzigen Meilenstein veröffentlichen konnten.

Also haben wir eine Lösung gefunden, die es uns ermöglichte, schrittweise Änderungen vorzunehmen. Dies wird weiter unten ausführlicher beschrieben. Kurz gesagt: Wir haben den Zwei-Phasen-Ansatz beibehalten, aber in der ersten Phase versuchen wir, Unterausdrücke mit dem Bottom-up-Abgleich abzugleichen (dadurch bricht der Regex-Ablauf), und in der zweiten Phase rendern wir die Top-down-Methode. In beiden Phasen konnten wir die vorhandenen Regex-basierten Matcher und Renderings verwenden – praktisch unverändert. So konnten wir sie nacheinander migrieren.

Phase 1: Bottom-up-Abgleich

In der ersten Phase wird mehr oder weniger genau und ausschließlich das getan, was auf dem Cover 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.

Betrachten Sie das obige Beispiel für einen Syntaxbaum:

Phase 1: Bottom-Up-Abgleich im Syntaxbaum.

Für diesen Baum würden unsere Übereinstimmer in der folgenden Reihenfolge angewendet:

  1. hsl(177degvar(--saturation, 100%) 50%): Zuerst sehen wir uns das erste Argument des hsl-Funktionsaufrufs an, den Farbtonwinkel. Wir gleichen ihn mit einem Winkel-Matcher ab, damit wir den Winkelwert mit dem Winkelsymbol versehen können.
  2. hsl(177degvar(--saturation, 100%)50%): Als Nächstes entdecken wir den var-Funktionsaufruf 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, über die wir später sprechen werden.
  3. hsl(177deg var(--saturation, 100%) 50%): Als Nächstes gleichen wir den Aufrufausdruck für die Funktion hsl 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 verbreiten die Ergebnisse in der Baumstruktur. 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. Daher können wir var-Funktionen einfach und kostengünstig durch ihre Ergebnisse ersetzen. So können wir wie in Schritt 2 triviale Fragen wie „Ruft das Resultat dieser var eine Farbe?“ beantworten.

Phase 2: Top-down-Rendering

In der zweiten Phase ändern wir die Richtung. Wir nehmen die Übereinstimmungsergebnisse aus Phase 1 und rendern den Baum in HTML, indem wir ihn von oben nach unten durchlaufen. Für jeden besuchten Knoten wird geprüft, ob eine Übereinstimmung vorliegt. Ist das der Fall, wird der entsprechende Renderer des Matchers aufgerufen. Wir vermeiden eine spezielle Behandlung von Knoten, die nur Text enthalten (z. B. NumberLiteral „50 %“), indem wir einen Standardabgleich und ‑renderer für Textknoten einbinden. Renderer geben einfach HTML-Knoten aus, die in Kombination die Darstellung des Property-Werts einschließlich seiner Dekorationen ergeben.

Phase 2: Top-down-Rendering im Syntaxbaum.

Im Beispielbaum wird der Property-Wert in der folgenden Reihenfolge gerendert:

  1. Rufen Sie den Funktionsaufruf hsl auf. Es gibt eine Übereinstimmung. Rufen Sie den Renderer der Farbfunktion auf. Sie bewirkt zweierlei:
    • Der tatsächliche Farbwert wird mit dem On-the-fly-Ersetzungsmechanismus für alle var-Argumente berechnet und dann wird ein Farbsymbol gezeichnet.
    • Rendert die untergeordneten Elemente von CallExpression rekursiv. Dadurch werden der Funktionsname, Klammern und Kommas automatisch gerendert, da es sich dabei nur um Text handelt.
  2. Rufen Sie das erste Argument des hsl-Aufrufs auf. Er stimmt überein. Rufen Sie daher den Winkel-Renderer auf, der das Winkelsymbol und den Winkeltext zeichnet.
  3. 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 ist. 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.
  4. Rufe das letzte Argument des hsl-Aufrufs auf. Es gibt keine Übereinstimmung. Daher wird nur der Textinhalt ausgegeben.

Haben Sie bemerkt, dass bei diesem Algorithmus ein Rendering 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 Regex-basierten zum Syntaxbaum-basierten Rendering. Wenn Knoten mit einem alten Regex-Matcher 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. Die Farbe, die durch das Farbsymbol angezeigt wird, hängt also vom Winkel ab, 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

Als wir uns dieses Problem genauer ansehen, um die Zuverlässigkeit zu verbessern und langjährige Probleme zu beheben, hatten wir einen Leistungsabfall erwartet, da wir angefangen haben, einen vollwertigen Parser auszuführen. 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 wir erwartet hatten, erwies sich der Parsing-basierte Ansatz in diesem Fall als 27% langsamer als der reguläre Regex-Ansatz. Bei dem auf regulären Ausdrücken basierenden Ansatz dauerte das Rendern 11 Sekunden, beim parserbasierten Ansatz 15 Sekunden zum Rendern.

Angesichts der Vorteile, die wir mit dem neuen Ansatz erzielen, haben wir uns entschieden, ihn weiter zu verfolgen.

Danksagungen

Wir danken Sofia Emelianova und Jecelyn Yeen für ihre unschätzbare Hilfe beim Bearbeiten dieses Beitrags.

Vorschaukanäle herunterladen

Verwenden Sie als Standard-Entwicklungsbrowser Chrome Canary, Chrome Dev oder Chrome Beta. Diese Vorabversionen bieten Zugriff auf die neuesten DevTools-Funktionen, ermöglichen den Test moderner Webplattform-APIs und helfen Ihnen, Probleme auf Ihrer Website zu finden, bevor Ihre Nutzer sie bemerken.

Chrome-Entwicklertools-Team kontaktieren

Verwende die folgenden Optionen, um die neuen Funktionen, Updates oder andere Aspekte der Entwicklertools zu besprechen.