正規表現以外の処理: Chrome DevTools の CSS 値の解析機能を強化

Philip Pfaffe
Ergün Erdogmus
Ergün Erdogmus

最近、Chrome DevTools の [Styles] タブの CSS プロパティが少し洗練されたように見えませんか?Chrome 121 から 128 にかけてリリースされたこれらのアップデートは、CSS 値の解析と表示方法の大幅な改善によるものです。この記事では、正規表現マッチング システムからより堅牢なパーサーへの移行という、この変換の技術的な詳細について説明します。

現在の DevTools と以前のバージョンを比較してみましょう。

上: 最新の Chrome、下: Chrome 121。

大きな違いですね。主な機能強化は次のとおりです。

  • color-mix。color-mix 関数内の 2 つの色引数を視覚的に表す便利なプレビュー。
  • pink。名前付きの色 pink のクリック可能な色のプレビュー。クリックするとカラー選択ツールが開き、簡単に調整できます。
  • var(--undefined, [fallback value])。未定義の変数の処理が改善され、未定義の変数がグレー表示され、アクティブなフォールバック値(この場合は HSL カラー)がクリック可能なカラー プレビューで表示されます。
  • hsl(…): hsl 色関数のクリック可能な別の色プレビュー。カラー選択ツールにすばやくアクセスできます。
  • 177deg: クリック可能な角度時計。角度の値をインタラクティブにドラッグして変更できます。
  • var(--saturation, …): カスタム プロパティ定義へのクリック可能なリンク。関連する宣言に簡単に移動できます。

その違いは顕著です。これを実現するには、DevTools が CSS プロパティ値を以前よりもはるかに深く理解できるようにする必要がありました。

これらのプレビューはすでに利用可能だったのでは?

これらのプレビュー アイコンは、見慣れたもののように思えるかもしれませんが、特に上記の例のような複雑な CSS 構文では、常に一貫して表示されるとは限りませんでした。また、機能する場合でも、正しく機能させるために多大な労力を要することがよくありました。

その理由は、値を分析するシステムが DevTools の初期から有機的に成長してきたためです。しかし、CSS の最近の素晴らしい新機能や、それに伴う言語の複雑さの増加に対応できていません。進化に対応するには、システムを全面的に再設計する必要がありました。そして、まさにそれを実現したのです。

CSS プロパティ値の処理方法

DevTools では、[スタイル] タブでプロパティ宣言をレンダリングして装飾するプロセスが、次の 2 つのフェーズに分かれています。

  1. 構造解析。この最初のフェーズでは、プロパティ宣言を分析して、基盤となるコンポーネントとその関係を特定します。たとえば、宣言 border: 1px solid red では、1px は長さ、solid は文字列、red は色として認識されます。
  2. レンダリング。構造分析に基づいて、レンダリング フェーズではこれらのコンポーネントを HTML 表現に変換します。これにより、表示されるプロパティ テキストにインタラクティブ要素と視覚的な手がかりが追加されます。たとえば、色の値 red は、クリック可能な色アイコンとともに表示されます。このアイコンをクリックすると、簡単に変更できるカラー選択ツールが表示されます。

正規表現

以前は、正規表現(regex)を使用してプロパティ値を分解し、構造分析を行っていました。Google では、装飾を検討したプロパティ値のビットに一致する正規表現のリストを保持していました。たとえば、CSS の色、長さ、角度、var 関数呼び出しなどのより複雑なサブ式に一致する式がありました。テキストを左から右にスキャンして値分析を行い、テキストの次の部分に一致するリストの最初の式を継続的に探しました。

ほとんどの場合はこれで問題ありませんでしたが、うまくいかないケースが増えていきました。これまで、マッチングが正しく行われなかったというバグレポートが多数寄せられていました。これらのバグを修正するにあたり(簡単な修正もあれば、かなり複雑な修正もありました)、技術的負債を抑えるためのアプローチを再考する必要がありました。いくつかの問題を見てみましょう。

color-mix() と一致

color-mix() 関数に使用した正規表現は次のとおりです。

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

構文は次のとおりです。

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

次の例を実行して、一致を可視化してみてください。

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

color-mix 関数のマッチ結果。

シンプルな例は問題なく動作します。しかし、より複雑な例では、<firstColor> の一致は hsl(177deg var(--saturation で、<secondColor> の一致は 100%) 50%)) であり、まったく意味がありません。

これは問題であると認識していました。結局のところ、正式な言語としての CSS は正規ではないため、var 関数などのより複雑な関数引数を処理するための特別な処理がすでに含まれています。しかし、最初のスクリーンショットでわかるように、それでもすべてのケースで機能するわけではありませんでした。

tan() と一致

報告されたバグの中で特に面白かったのは、三角関数 tan() に関するものでした。色を照合するために使用していた正規表現には、red キーワードなどの名前付きの色を照合するためのサブ式 \b[a-zA-Z]+\b(?!-) が含まれていました。次に、一致した部分が実際に名前付きの色であるかどうかを確認したところ、tan も名前付きの色であることがわかりました。そのため、tan() 式が色として誤って解釈されていました。

var() と一致

別の例として、他の var() 参照を含むフォールバックを持つ var() 関数を見てみましょう。var(--non-existent, var(--margin-vertical))

var() の正規表現は、この値に一致します。ただし、最初の閉じ括弧で一致が停止します。したがって、上記のテキストは var(--non-existent, var(--margin-vertical) と一致します。これは、正規表現マッチングの教科書的な制限です。一致する括弧を必要とする言語は、基本的に正規ではありません。

CSS パーサーに移行する

正規表現を使用したテキスト分析が機能しなくなった場合(分析対象の言語が正規でないため)、正規の次のステップとして、より高度な文法に対応するパーサーを使用します。CSS の場合、これはコンテキストフリー言語に対応するパーサーを意味します。実際、このようなパーサー システムは DevTools のコードベースにすでに存在していました。CodeMirror の Lezer です。これは、[Sources] パネルにあるエディタである CodeMirror の構文ハイライトなどの基盤となっています。Lezer の CSS パーサーを使用すると、CSS ルールの(抽象ではない)構文ツリーを生成でき、すぐに使用できる状態になりました。勝利。

プロパティ値 hsl(177deg var(--saturation, 100%) 50%) の構文ツリー。これは、Lezer パーサーによって生成された結果を簡略化したもので、カンマと括弧の純粋な構文ノードは省略されています。

ただし、正規表現ベースのマッチングからパーサーベースのマッチングに直接移行することは、そのままでは不可能であることがわかりました。この 2 つのアプローチは、逆の方向から機能するためです。正規表現を使用して値の一部を照合する場合、DevTools は入力を左から右にスキャンし、パターンの順序付きリストから最初の一致を繰り返し検索します。構文ツリーを使用すると、一致はボトムアップで開始されます。たとえば、関数呼び出しを一致させる前に、まず呼び出しの引数を分析します。これは、算術式を評価する際に、まず括弧で囲まれた式を考慮し、次に乗算演算子、加算演算子の順に考慮するのと同様です。このフレームワークでは、正規表現ベースのマッチングは、算術式を左から右に評価することに対応します。マッチング システム全体をゼロから書き直すことは、本当に避けたいと考えていました。15 種類のマッチャーとレンダラのペアがあり、数千行のコードが関連付けられていたため、1 つのマイルストーンでリリースすることは不可能に思えました。

そこで、段階的な変更を可能にするソリューションを考案しました。これについては、以下で詳しく説明します。つまり、2 段階のアプローチは維持しましたが、第 1 段階では、サブ式をボトムアップで照合しようとし(正規表現フローを中断)、第 2 段階では、トップダウンでレンダリングします。どちらのフェーズでも、既存の正規表現ベースのマッチャーとレンダラーをほぼそのまま使用できたため、1 つずつ移行することができました。

フェーズ 1: ボトムアップ マッチング

最初のフェーズでは、ほぼ完全に、そして排他的に、その名のとおりの処理を行います。ツリーを下から上に順番に走査し、アクセスした各構文ツリーノードで部分式の一致を試みます。特定のサブ式を照合するために、マッチャーは既存のシステムと同様に正規表現を使用できます。バージョン 128 の時点では、長さの一致など、いくつかのケースでまだ使用されています。また、マッチャーは、現在のノードをルートとするサブツリーの構造を分析することもできます。これにより、構文エラーをキャッチすると同時に、構造情報を記録できます。

上記の構文ツリーの例について考えてみましょう。

フェーズ 1: 構文ツリーでのボトムアップ マッチング。

このツリーでは、マッチャーは次の順序で適用されます。

  1. hsl(177degvar(--saturation, 100%) 50%): まず、hsl 関数呼び出しの最初の引数である色相角を検出します。角度マッチャーと照合して、角度値に角度アイコンを装飾できるようにします。
  2. hsl(177degvar(--saturation, 100%)50%): 次に、var マッチャーを使用して var 関数呼び出しを検出します。このような呼び出しでは、主に次の 2 つのことを行います。
    • 変数の宣言を検索してその値を計算し、変数名にリンクとポップオーバーを追加して、それぞれに接続します。
    • 計算された値が色の場合、呼び出しを色アイコンで装飾します。実は 3 つ目もありますが、これについては後ほど説明します。
  3. hsl(177deg var(--saturation, 100%) 50%): 最後に、hsl 関数の呼び出し式を照合して、色アイコンで装飾できるようにします。

装飾する部分式を検索するだけでなく、実際には、マッチング プロセスの一環として実行している 2 つ目の機能があります。ステップ 2 で、変数名の計算値を検索すると説明しました。実際には、さらに一歩進んで、結果をツリーの上位に伝播します。変数だけでなく、代替値にも適用されます。var 関数ノードにアクセスすると、その子は事前にアクセスされていることが保証されるため、フォールバック値に表示される可能性のある var 関数の結果はすでにわかっています。そのため、var 関数の結果をその場で簡単かつ安価に置き換えることができます。これにより、ステップ 2 で行ったように、「この var 呼び出しの結果は色ですか?」などの質問に簡単に答えることができます。

フェーズ 2: トップダウン レンダリング

第 2 フェーズでは、方向を逆にします。フェーズ 1 の一致結果を取得し、ツリーを上から下に順番に走査して HTML にレンダリングします。訪問したノードごとに、一致するかどうかを確認し、一致する場合は、マッチャーの対応するレンダラを呼び出します。テキストノード用のデフォルトのマッチャーとレンダラを含めることで、テキストのみを含むノード(NumberLiteral「50%」など)を特別に処理する必要がなくなります。レンダラは HTML ノードを出力するだけで、それらを組み合わせることで、装飾を含むプロパティ値の表現が生成されます。

フェーズ 2: 構文ツリーのトップダウン レンダリング。

このツリーの例では、プロパティ値は次の順序でレンダリングされます。

  1. hsl 関数呼び出しにアクセスします。一致したため、色関数レンダラを呼び出します。次の 2 つの処理を行います。
    • var 引数のオンザフライ置換メカニズムを使用して実際の色値を計算し、色アイコンを描画します。
    • CallExpression の子を再帰的にレンダリングします。これにより、関数名、かっこ、カンマ(単なるテキスト)のレンダリングが自動的に処理されます。
  2. hsl 呼び出しの最初の引数を参照します。一致したため、角度レンダラを呼び出します。角度レンダラは、角度アイコンと角度のテキストを描画します。
  3. 2 番目の引数(var 呼び出し)にアクセスします。一致したため、var renderer を呼び出します。出力は次のようになります。
    • 先頭のテキスト var(。
    • 変数名。変数の定義へのリンク、または未定義であることを示すグレーのテキスト色で装飾されます。また、変数の値に関する情報を表示するポップオーバーも追加されます。
    • カンマ、フォールバック値の順に再帰的にレンダリングします。
    • 閉じかっこ。
  4. hsl 呼び出しの最後の引数にアクセスします。一致しなかったため、テキスト コンテンツのみを出力します。

このアルゴリズムでは、一致したノードの子のレンダリング方法をレンダラーが完全に制御していることに気づきましたか?子を再帰的にレンダリングすることは、プロアクティブな処理です。この方法により、正規表現ベースのレンダリングから構文ツリーベースのレンダリングへの段階的な移行が可能になりました。以前の regex-matcher と一致するノードの場合、対応するレンダラを元の形式で使用できます。構文ツリーの観点から見ると、このコンポーネントはサブツリー全体のレンダリングを担当し、その結果(HTML ノード)は周囲のレンダリング プロセスに適切に組み込まれます。これにより、マッチャーとレンダラーをペアで移植し、1 つずつ入れ替えることができました。

一致したノードの子のレンダリングを制御するレンダラのもう 1 つの優れた機能は、追加するアイコン間の依存関係を推論できることです。上記の例では、hsl 関数によって生成される色は、明らかに色相値に依存しています。つまり、色アイコンで表示される色は、角度アイコンで表示される角度によって異なります。ユーザーがそのアイコンから角度エディタを開いて角度を変更すると、カラーアイコンの色をリアルタイムで更新できるようになりました。

上記の例のように、このメカニズムは他のアイコンのペア設定にも使用されます。たとえば、color-mix() とその 2 つのカラー チャンネル、またはフォールバックから色を返す var 関数などです。

パフォーマンスへの影響

信頼性を向上させ、長年の問題を解決するためにこの問題に取り組んだとき、本格的なパーサーの実行を開始したことを考えると、パフォーマンスの低下が予想されました。これをテストするために、約 3, 500 個のプロパティ宣言をレンダリングするベンチマークを作成し、M1 マシンで 6 倍のスロットリングを使用して、正規表現ベースのバージョンとパーサーベースのバージョンの両方をプロファイリングしました。

予想どおり、このケースでは、解析ベースのアプローチは正規表現ベースのアプローチよりも 27% 遅いことがわかりました。正規表現ベースのアプローチではレンダリングに 11 秒、パーサーベースのアプローチではレンダリングに 15 秒かかりました。

新しいアプローチから得られるメリットを考慮して、このアプローチを進めることにしました。

謝辞

この投稿の編集に多大なご協力をいただいた Sofia Emelianova と Jecelyn Yeen に心より感謝いたします。

プレビュー チャネルをダウンロードする

デフォルトの開発用ブラウザとして、Chrome の Canary、Dev、Beta のいずれかを使用することをおすすめします。これらのプレビュー チャンネルでは、最新の DevTools 機能にアクセスしたり、最先端のウェブ プラットフォーム API をテストしたり、ユーザーが気づく前にサイトの問題を見つけたりできます。

Chrome DevTools チームに問い合わせる

DevTools の新機能、更新、その他の関連事項について話し合うには、次のオプションを使用します。