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

Philip Pfaffe
Ergün Erdogmus
Ergün Erdogmus

Kennst du die CSS-Eigenschaften in den Chrome-Entwicklertools? Der Tab Stile hat sich in letzter Zeit etwas ausgefeilter gestaltet? 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 Entwicklertools mit der vorherigen Version:

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

Das ist doch ein großer Unterschied, oder? Die wichtigsten Verbesserungen:

  • color-mix Eine praktische Vorschau, die die beiden Farbargumente in der color-mix-Funktion visuell darstellt.
  • pink Eine anklickbare Farbvorschau für die benannte Farbe pink. Klicken Sie darauf, um eine Farbauswahl für einfache Anpassungen zu öffnen.
  • var(--undefined, [fallback value]) Die Verarbeitung nicht definierter Variablen wurde verbessert, wobei die undefinierte Variable ausgegraut und der aktive Fallback-Wert (in diesem Fall eine HSL-Farbe) mit einer anklickbaren Farbvorschau angezeigt wird.
  • 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 beeindruckend. Dazu mussten wir den Entwicklertools wesentlich beibringen, CSS-Eigenschaftswerte besser zu verstehen.

Waren diese Vorschauen nicht schon verfügbar?

Diese Vorschausymbole scheinen Ihnen zwar vertraut zu sein, wurden aber nicht immer einheitlich angezeigt, insbesondere in 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. Es konnte jedoch nicht mit den neuesten tollen neuen Funktionen von CSS und der entsprechenden Zunahme der Sprachkomplexität 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 den Entwicklertools ist der Prozess zum Rendern und Dekorieren von Eigenschaftsdeklarationen auf dem Tab Styles in zwei verschiedene Phasen unterteilt:

  1. Strukturelle Analyse. In dieser Anfangsphase wird die Eigenschaftsdeklaration 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. Basierend auf der strukturellen Analyse werden diese Komponenten bei der Rendering-Phase in eine HTML-Darstellung umgewandelt. Dadurch wird der angezeigte Eigenschaftstext um interaktive Elemente und visuelle Hinweise ergänzt. Der Farbwert red wird beispielsweise mit einem anklickbaren Farbsymbol gerendert, das beim Anklicken eine Farbauswahl anzeigt, die leicht geändert werden kann.

Reguläre Ausdrücke

Zuvor haben wir uns mit regulären Ausdrücken (Regexes) beschäftigt, um die Eigenschaftswerte für die Strukturanalyse zu zerlegen. Wir führten eine Liste mit regulären Ausdrücken, um die Teile von Property-Werten abzugleichen, die wir als Dekoration betrachteten. Es gibt beispielsweise Ausdrücke, die mit CSS-Farben, Längen, Winkeln oder komplizierteren Unterausdrücken wie var-Funktionsaufrufen übereinstimmen. 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.

Dies funktionierte zwar meistens gut, aber die Zahl der Fälle, in denen es nicht weiter anstieg, stieg nicht weiter an. Im Laufe der Jahre haben wir sehr viele Fehlerberichte erhalten, bei denen die Übereinstimmung nicht geklappt hat. Nachdem wir sie korrigiert hatten – einige einfach, andere recht aufwendig – mussten wir unseren Ansatz überdenken, um unsere technischen Schulden fernzuhalten. Sehen wir uns einige der Probleme an.

Übereinstimmung mit color-mix()

Der Regex, den wir für die Funktion color-mix() verwendet haben, war:

/color-mix\(.*,\s*(?<firstColor>.+)\s*,\s*(?<secondColor>.+)\s*\)/g

Dies entspricht der Syntax:

color-mix(<color-interpolation-method>, [<color> && <percentage [0,100]>?]#{2})

Versuchen Sie, das folgende Beispiel auszuführen, 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);

Stimmt mit dem Ergebnis für die Funktion „color-mix“ überein.

Das einfachere Beispiel funktioniert problemlos. 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 war. Schließlich ist CSS als formelle Sprache nicht normal. Daher haben wir bereits eine spezielle Handhabung für kompliziertere Funktionsargumente, wie var-Funktionen, eingeführt. Wie Sie im ersten Screenshot sehen können, hat dies jedoch immer noch nicht in allen Fällen funktioniert.

Übereinstimmung mit tan()

Einer der verrücktesten gemeldeten Fehler war die trigonometrische Funktion tan() . Der reguläre Ausdruck, den wir zum Abgleichen von Farben verwendet haben, enthielt den Unterausdruck \b[a-zA-Z]+\b(?!-) für den Abgleich benannter Farben wie dem red-Keyword. Dann haben wir geprüft, ob der übereinstimmende Teil tatsächlich eine benannte Farbe ist. Und raten Sie, tan ist auch eine benannte Farbe! Deshalb haben wir tan()-Ausdrücke fälschlicherweise als Farben interpretiert.

Übereinstimmung mit var()

Sehen wir uns ein weiteres Beispiel an: var()-Funktionen mit einem Fallback, das andere var()-Referenzen enthält: var(--non-existent, var(--margin-vertical)).

Unser regulärer Ausdruck für var() würde mit diesem Wert übereinstimmen. Allerdings wird der Abgleich bei der ersten schließenden Klammer beendet. Der obige Text wird also als var(--non-existent, var(--margin-vertical) abgeglichen. Dies ist eine Lehrbucheinschränkung für den Abgleich regulärer Ausdrücke. Sprachen, für die eine übereinstimmende Klammer erforderlich ist, sind grundsätzlich nicht regulär.

Übergang zu einem 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 höhere Grammatik. Bei CSS bedeutet dies einen Parser für kontextfreie Sprachen. Ein solches Parsersystem gab es bereits in der Entwicklertools-Codebasis: Lezer von CodeMirror, das beispielsweise die Grundlage für die Syntaxhervorhebung in CodeMirror bildet, dem Editor, den Sie im Bereich Quellen finden. Mit dem CSS-Parser von Lezer konnten wir (nicht abstrakte) Syntaxbäume für CSS-Regeln erstellen. Der Parser war sofort einsatzbereit. Sieg.

Ein Syntaxbaum für den Attributwert „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.

Wir haben jedoch festgestellt, dass eine direkte Migration vom Regex-basierten auf den Parser-basierten Abgleich nicht möglich war: Die beiden Ansätze arbeiten von gegensätzlichen Richtungen aus. Beim Abgleichen von Werten mit regulären Ausdrücken scannt Entwicklertools die Eingabe von links nach rechts und versucht wiederholt, die früheste Übereinstimmung aus einer sortierten Liste von Mustern zu finden. Bei einem Syntaxbaum beginnt der Abgleich von unten nach oben, z. B. werden zuerst die Argumente eines Aufrufs analysiert, bevor versucht wird, den Funktionsaufruf abzugleichen. Sie können sich dies als Bewertung eines arithmetischen Ausdrucks vorstellen, bei dem zuerst geklammerte Ausdrücke, dann multiplikative und additive Operatoren betrachtet werden. In diesem Framing entspricht der auf Regex basierende Abgleich der Auswertung des arithmetischen Ausdrucks von links nach rechts. Wir wollten nicht das gesamte Abgleichsystem von Grund auf neu schreiben: Es gab 15 verschiedene Matcher und Renderer-Paare mit Tausenden von Codezeilen. Daher war es unwahrscheinlich, 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, und sie 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 Cover steht. Wir durchlaufen die Baumstruktur von unten nach oben und versuchen, Unterausdrücke an jedem Syntaxbaumknoten abzugleichen, den wir besuchen. Um einen bestimmten Unterausdruck abzugleichen, kann ein Matcher wie im bestehenden System reguläre Ausdrücke verwenden. Ab Version 128 tun wir dies in einigen Fällen immer noch, z. B. bei übereinstimmenden Längen. Alternativ kann ein Matcher die Struktur der Unterstruktur des aktuellen Knotens analysieren. So lassen sich Syntaxfehler abfangen und gleichzeitig strukturelle Informationen aufzeichnen.

Betrachten Sie das obige Beispiel für einen Syntaxbaum:

Phase 1: Bottom-up-Abgleich im Syntaxbaum.

Auf diese Baumstruktur würden unsere Matcher in der folgenden Reihenfolge angewendet:

  1. hsl(177degvar(--saturation, 100%) 50%): Zuerst entdecken wir das erste Argument des Funktionsaufrufs hsl, 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. Für solche Anrufe sind in erster Linie zwei Dinge wichtig: <ph type="x-smartling-placeholder">
      </ph>
    • Suchen Sie die Deklaration der Variablen, berechnen Sie ihren Wert und fügen Sie dem Variablennamen einen Link bzw. ein Pop-over hinzu, um jeweils eine Verbindung herzustellen.
    • Gestalte den Aufruf mit einem Farbsymbol, wenn der berechnete Wert eine Farbe ist. Es gibt noch eine dritte Sache, die wir später besprechen werden.
  3. hsl(177deg var(--saturation, 100%) 50%): Zuletzt gleichen wir den Aufrufausdruck für die hsl-Funktion ab, damit wir ihn mit dem Farbsymbol dekorieren können.

Neben der Suche nach Unterausdrücken, die wir dekorieren möchten, gibt es noch eine zweite Funktion, die wir im Rahmen des Abgleichsprozesses ausführen. Beachten Sie, dass wir in Schritt 2 den berechneten Wert für einen Variablennamen abgerufen haben. Wir gehen sogar noch einen Schritt weiter und verbreiten die Ergebnisse in der Baumstruktur. Dies gilt nicht nur für die Variable, sondern auch für den Fallback-Wert. Beim Aufrufen eines var-Funktionsknotens wird sichergestellt, dass dessen untergeordnete Elemente vorher besucht werden. Daher kennen wir die Ergebnisse von var-Funktionen, die im Fallback-Wert enthalten sein könnten, bereits. 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 kehren wir die Richtung um. Wir nehmen die Übereinstimmungsergebnisse aus Phase 1 und rendern den Baum in HTML, indem wir ihn von oben nach unten durchlaufen. Wir prüfen für jeden besuchten Knoten, ob eine Übereinstimmung gefunden wurde, und rufen dann den entsprechenden Renderer des Matchers auf. Bei Knoten, die nur Text enthalten (z. B. NumberLiteral „50 %“), ist eine besondere Behandlung nicht erforderlich. Dazu fügen wir einen Standard-Matcher und Renderer für Textknoten ein. Renderer geben einfach HTML-Knoten aus, die zusammen die Darstellung des Eigenschaftswerts erzeugen, einschließlich seiner Dekoration.

Phase 2: Top-down-Rendering im Syntaxbaum.

Im Beispiel unten sehen Sie die Reihenfolge, in der der Eigenschaftswert gerendert wird:

  1. Rufen Sie den Funktionsaufruf hsl auf. Es stimmt überein, also rufen Sie den Renderer für die Farbfunktion auf. Er bewirkt zwei Dinge: <ph type="x-smartling-placeholder">
      </ph>
    • Berechnet den tatsächlichen Farbwert mithilfe des spontanen Ersetzungsmechanismus für alle var-Argumente und zeichnet dann ein Farbsymbol.
    • Rendert die untergeordneten Elemente von CallExpression rekursiv. Dadurch werden der Funktionsname, die Klammern und die Kommas, bei denen es sich nur um Text handelt, automatisch gerendert.
  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, den var-Aufruf. Die Übereinstimmung stimmt überein. Rufen Sie daher die Variable renderer auf, die Folgendes ausgibt: <ph type="x-smartling-placeholder">
      </ph>
    • Der Text var( zu Beginn.
    • Der Variablenname und verziert sie entweder mit einem Link zur Variablendefinition oder mit einer grauen Textfarbe, um anzuzeigen, dass sie nicht definiert wurde. Außerdem wird der Variablen ein Popover hinzugefügt, in dem Informationen über ihren Wert angezeigt werden.
    • Das Komma (Semikolon) und der Fallback-Wert wird dann rekursiv gerendert.
    • Eine schließende Klammer.
  4. Rufen Sie das letzte Argument des hsl-Aufrufs auf. Keine Übereinstimmung, geben Sie einfach den Textinhalt aus.

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 erfolgt 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 Kontext des Syntaxbaums würde es die Verantwortung für das Rendern der gesamten Unterstruktur übernehmen und ihr Ergebnis (ein HTML-Knoten) könnte sauber in den umgebenden Renderingprozess eingesteckt werden. Dadurch konnten wir Matcher und Renderer paarweise portieren und nacheinander austauschen.

Eine weitere coole Funktion von Renderern, die das Rendering der untergeordneten Knoten ihres übereinstimmenden Knotens steuern kann, ist die Möglichkeit, Abhängigkeiten zwischen den Symbolen zu erkennen, die wir hinzufügen. Im obigen Beispiel hängt die von der hsl-Funktion erzeugte Farbe offensichtlich von ihrem Farbtonwert ab. Die durch das Farbsymbol angezeigte Farbe hängt also vom Winkel ab, der durch das Winkelsymbol angezeigt wird. Wenn der Nutzer den Winkeleditor über dieses Symbol öffnet und den Winkel ändert, können wir die Farbe des Farbsymbols in Echtzeit aktualisieren:

Wie Sie im obigen Beispiel sehen können, verwenden wir diesen Mechanismus auch für andere Symbolpaare, z. B. für color-mix() und dessen zwei Farbkanäle oder var-Funktionen, die eine Farbe aus ihrem Fallback zurückgeben.

Auswirkungen auf die Leistung

Als wir uns mit diesem Problem befassten, um die Zuverlässigkeit zu verbessern und langjährige Probleme zu beheben, hatten wir eine Leistungsbeeinträchtigung erwartet, da wir angefangen haben, einen vollwertigen Parser auszuführen. Um dies zu testen, haben wir eine Benchmark erstellt, die etwa 3.500 Eigenschaftsdeklarationen darstellt. Außerdem haben wir für die Regex-basierten und Parser-basierten Versionen ein Profil mit einer 6-fachen Drosselung auf einem M1-Computer erstellt.

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.

In Anbetracht der Vorteile, die wir mit dem neuen Ansatz erzielen, haben wir beschlossen, diesen weiterzuverfolgen.

Danksagungen

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

Vorschaukanäle herunterladen

Sie können Canary, Dev oder Beta als Standardbrowser für die Entwicklung verwenden. Über diese Vorschaukanäle erhalten Sie Zugriff auf die neuesten Entwicklertools, können innovative Webplattform-APIs testen und Probleme auf Ihrer Website erkennen, bevor Ihre Nutzer es tun.

Kontaktaufnahme mit dem Team für Chrome-Entwicklertools

Mit den folgenden Optionen kannst du die neuen Funktionen und Änderungen in dem Beitrag oder andere Aspekte der Entwicklertools besprechen.

  • Senden Sie uns über crbug.com einen Vorschlag oder Feedback.
  • Problem mit den Entwicklertools über Weitere Optionen melden Mehr > Hilfe > Hier kannst du Probleme mit den Entwicklertools in den Entwicklertools melden.
  • Twittern Sie unter @ChromeDevTools.
  • Hinterlasse Kommentare in den YouTube-Videos mit den Neuerungen in den Entwicklertools oder in YouTube-Videos mit Tipps zu den Entwicklertools.