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

Philip Pfaffe
Ergün Erdogmus
Ergün Erdogmus

Chrome DevTools の [Styles] タブの CSS プロパティが最近少し洗練されたように見えるでしょうか?Chrome 121 ~ 128 でリリースされたこれらの更新は、CSS 値の解析と表示方法が大幅に改善された結果です。この記事では、正規表現マッチング システムからより堅牢なパーサーに移行するこの変革の技術的な詳細を解説します。

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

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

かなりの違いがありますね。主な機能強化は次のとおりです。

  • color-mixcolor-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)を使用してプロパティ値を分解し、構造分析を行っていました。デコレーションの対象となるプロパティ値のビットに対応する正規表現のリストを維持しました。たとえば、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%)) です。これはまったく意味がありません。

Google はこの問題を認識しており、結局のところ、正式な言語としての CSS は正規ではないため、var 関数など、より複雑な関数引数を処理するための特別な処理がすでに含まれています。しかし、1 つ目のスクリーンショットからわかるように、この方法でもうまくいかなかったことがあります。

tan() と一致

報告されているおもしろいバグの 1 つが、三角関数の 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 です。これは、[ソース] パネルにあるエディタである 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 では、変数名の計算値を検索すると説明しました。実際、Google はさらに一歩進んで、結果をツリー上に伝播します。変数だけでなく、代替値にも適用されます。var 関数ノードにアクセスするときに、その子ノードに事前にアクセスされているため、フォールバック値に表示される可能性がある var 関数の結果はすでにわかっています。そのため、var 関数をその結果に簡単に、安価に置き換えることができます。これにより、ステップ 2 で行ったように、「この var 呼び出しの結果は色ですか?」などの質問に簡単に答えることができます。

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

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

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

この例のツリーの場合、プロパティ値がレンダリングされる順序は次のとおりです。

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

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

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

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

パフォーマンスへの影響

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

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

新しいアプローチによる成果を考慮して、この手法を採用することにしました。

謝辞

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

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

デフォルトの開発用ブラウザとして Chrome の CanaryDevBeta を使用することを検討してください。これらのプレビュー チャンネルでは、最新の DevTools 機能にアクセスしたり、最先端のウェブ プラットフォーム API をテストしたりできます。また、ユーザーよりも早くサイトの問題を見つけることもできます。

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

次のオプションを使用して、DevTools の新機能、更新、その他のトピックについて話し合います。