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

Philip Pfaffe
Ergün Erdogmus
Ergün Erdogmus

Chrome DevTools の 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, …): カスタム プロパティの定義へのクリック可能なリンク。関連する宣言に簡単に移動できます。

大きな違いです。これを実現するには、CSS プロパティの値を以前より深く理解できるように DevTools に学習させる必要がありました。

これらのプレビューはすでに利用可能ではありませんでしたか?

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

その理由は、値を分析するシステムが DevTools の登場当初から有機的に拡大してきたからです。しかし、CSS で提供される最近の素晴らしい新機能や、それに伴って言語が複雑化することに追い付いていません。進化に対応するためにシステムを完全に再設計する必要があったのです。まさに Google がそれを実現しました。

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

DevTools の [Styles] タブでのプロパティ宣言のレンダリングとデコレーションのプロセスは、次の 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);

カラーミックス関数の照合結果。

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

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

tan() に一致

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

var() に一致

別の例(var(--non-existent, var(--margin-vertical)))を見てみましょう。他の var() 参照を含むフォールバックを使用する var() 関数です。

var() の正規表現は、この値と一致します。例外として、最初の閉じかっこで照合が停止されます。したがって、上記のテキストは var(--non-existent, var(--margin-vertical) と照合されます。これは、正規表現一致の教科書に制限されている点です。括弧を一致させる必要がある言語は、基本的に通常の言語ではありません。

CSS パーサーへの移行

正規表現を使用したテキスト分析が機能しなくなった場合(分析対象の言語が正規言語ではないため)は、正規の次のステップがあります。それは、上位型の文法のパーサーを使用することです。これは、CSS ではコンテキストフリー言語のパーサーを意味します。実際、このようなパーサー システムである CodeMirror の Lezer は、DevTools のコードベースにすでに存在していました。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 関数呼び出しを確認します。このようなコールでは、主に次の 2 つのことを行います。 <ph type="x-smartling-placeholder">
      </ph>
    • 変数の宣言を検索して値を計算し、変数名にリンクとポップオーバーを追加してそれぞれに接続します。
    • 計算された値が色の場合は、呼び出しを色アイコンで装飾します。 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 つの処理を行います。 <ph type="x-smartling-placeholder">
      </ph>
    • var 引数のオンザフライ置換メカニズムを使用して実際の色値を計算し、カラーアイコンを描画します。
    • CallExpression の子を再帰的にレンダリングします。これにより、関数名、かっこ、カンマ(単なるテキスト)が自動的にレンダリングされます。
  2. hsl 呼び出しの最初の引数にアクセスします。一致したので、アングル レンダラを呼び出して、アングルのアイコンとテキストを描画します。
  3. 2 番目の引数(var 呼び出し)にアクセスします。一致したので、var renderer を呼び出します。次の出力が出力されます。 <ph type="x-smartling-placeholder">
      </ph>
    • 先頭の var( というテキスト。
    • 変数名が入力され、変数の定義へのリンクか、未定義であることを示す灰色のテキストで装飾されます。また、変数にポップオーバーを追加して、値に関する情報を表示します。
    • カンマで指定し、フォールバック値を再帰的にレンダリングします。
    • 閉じかっこ。
  4. hsl 呼び出しの最後の引数にアクセスします。一致しなかったので、テキストの内容を出力します。

このアルゴリズムでは、一致したノードの子がどのようにレンダリングされるかを、レンダリングによって完全に制御していることにお気づきでしょうか。子を再帰的にレンダリングするのはプロアクティブです。このトリックにより、正規表現ベースのレンダリングから構文ツリーベースのレンダリングに段階的に移行できるようになりました。以前の正規表現マッチャーに一致するノードについては、対応するレンダラを元の形式で使用できます。構文ツリーの用語では、サブツリー全体のレンダリングを担う必要があり、その結果(HTML ノード)は周囲のレンダリング プロセスに適切にプラグインできます。これにより、マッチャーとレンダラをペアで移植し、1 つずつ入れ替えることができました。

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

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

パフォーマンスへの影響

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

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

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

謝辞

この投稿を編集してくれた Sofia Emelianova と Jecelyn Yeen に深く感謝します。

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

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

Chrome DevTools チームへのお問い合わせ

この投稿で紹介する新機能や変更点、またはその他の DevTools に関連する内容については、以下のオプションを使って議論してください。

  • ご提案やフィードバックは、crbug.com からお送りください。
  • DevTools の問題を報告するには、その他のオプションもっと見る) >ヘルプ >DevTools で DevTools の問題を報告します。
  • @ChromeDevTools でツイートしてください。
  • DevTools の新機能に関する YouTube 動画または DevTools のヒントの YouTube 動画にコメントを残してください。