DevTools の設計: AI アシスタンスでの効率的なトークン使用

公開日: 2026 年 1 月 30 日

パフォーマンスの AI アシスタンスを構築するうえで、主なエンジニアリング上の課題は、DevTools で記録されたパフォーマンス トレースを Gemini で適切に処理することでした。

大規模言語モデル(LLM)は「コンテキスト ウィンドウ」内で動作します。これは、一度に処理できる情報量に厳しい制限があることを意味します。この容量はトークン単位で測定されます。Gemini モデルの場合、1 つのトークンは約 4 文字のグループです。

パフォーマンス トレースは、多くの場合数メガバイトで構成される巨大な JSON ファイルです。未加工のトレースを送信すると、モデルのコンテキスト ウィンドウがすぐにいっぱいになり、質問の余地がなくなります。

パフォーマンスに関する AI アシスタンスを実現するため、トークン使用量を最小限に抑えながら、LLM にとって有用なデータ量を最大化するシステムを設計する必要がありました。このブログでは、そのために使用した手法について説明し、お客様のプロジェクトで採用できるようにします。

初期コンテキストを調整する

ウェブサイトのパフォーマンスのデバッグは複雑な作業です。デベロッパーは、コンテキストとしてトレース全体を確認することも、コア ウェブ バイタルとトレースの関連する期間に焦点を当てることも、詳細にまで踏み込んでクリックやスクロールなどの個々のイベントとその関連するコールスタックに焦点を当てることもできます。

デバッグ プロセスを支援するため、DevTools の AI アシスタンスは、デベロッパーのジャーニーに一致し、関連するデータのみを使用して、デベロッパーの焦点に固有のアドバイスを提供する必要があります。そのため、常に完全なトレースを送信するのではなく、デバッグ タスクに基づいてデータをスライスする AI アシスタンスを構築しました。

デバッグタスク AI アシスタンスに最初に送信されたデータ
パフォーマンス トレースについてチャットする トレースの概要: トレースとデバッグ セッションの概要情報を含むテキストベースのレポート。ページの URL、スロットリング条件、主要なパフォーマンス指標(LCP、INP、CLS)、利用可能な分析情報のリスト、CrUX の概要(利用可能な場合)が含まれます。
パフォーマンスの分析情報についてチャットする トレースの概要と、選択したパフォーマンス分析情報の名前。
トレースのタスクについてチャットする トレースの概要と、選択したタスクが配置されているシリアル化されたコールツリー。
ネットワーク リクエストについてチャットする トレースの概要、選択したリクエストキーとタイムスタンプ
トレース アノテーションを生成する 選択したタスクが配置されているシリアル化されたコールツリー。シリアル化されたツリーは、選択されたタスクを識別します。

トレースの概要は、AI アシスタンスの基盤となるモデルである Gemini に初期コンテキストを提供するために、ほぼ常に送信されます。AI 生成のアノテーションの場合、これは省略されます。

AI にツールを提供する

DevTools の AI アシスタンスはエージェントとして機能します。つまり、デベロッパーの最初のプロンプトと共有された最初のコンテキストに基づいて、より多くのデータを自律的にクエリできます。より多くのデータをクエリするために、AI アシスタンスに呼び出すことができる一連の事前定義関数を提供しました。関数呼び出しまたはツール使用と呼ばれるパターン。

前述のデバッグ ジャーニーに基づいて、エージェントの一連の粒度の細かい関数を定義しました。これらの関数は、人間のデベロッパーがパフォーマンスのデバッグに取り組むのと同様に、初期コンテキストに基づいて重要と見なされる詳細を掘り下げます。関数セットは次のとおりです。

関数 説明
getInsightDetails(name) 特定のパフォーマンス分析情報(LCP にフラグが付けられた理由など)に関する詳細情報を返します。
getEventByKey(key) 単一の特定のイベントの詳細なプロパティを返します。
getMainThreadTrackSummary(start, end) トップダウン、ボトムアップ、サードパーティの概要など、指定された範囲のメインスレッド アクティビティの概要を返します。
getNetworkTrackSummary(start, end) 指定された期間のネットワーク アクティビティの概要を返します。
getDetailedCallTree(event_key) パフォーマンス トレースの特定のメインスレッド イベントの完全なコールツリーを返します
getFunctionCode(url, line, col) リソース内の特定の場所で定義された関数のソースコードを返します。このソースコードには、パフォーマンス トレースのランタイム パフォーマンス データがアノテーションとして付加されています。
getResourceContent(url) ページで使用されるテキスト リソース(HTML や CSS など)のコンテンツを返します。

データ取得をこれらの関数呼び出しに厳密に制限することで、関連性の高い情報のみが明確に定義された形式でコンテキスト ウィンドウに入り、トークンの使用が最適化されます。

エージェント オペレーションの例

AI アシスタンスが関数呼び出しを使用して詳細情報を取得する実用的な例を見てみましょう。「このリクエストが遅いのはなぜですか?」という最初のプロンプトの後に、AI 支援は、次の関数を段階的に呼び出すことができます。

  1. getEventByKey: ユーザーが選択した特定のリクエストの詳細なタイミングの内訳(TTFB、ダウンロード時間)を取得します。
  2. getMainThreadTrackSummary: リクエストが開始されるはずだったときに、メインスレッドがビジー状態(ブロック状態)だったかどうかを確認します。
  3. getNetworkTrackSummary: 他のリソースが同時に帯域幅を競合していたかどうかを分析します。
  4. getInsightDetails: トレースの概要に、このリクエストに関連するボトルネックに関する分析情報がすでに記載されているかどうかを確認します。

これらの呼び出しの結果を組み合わせることで、AI アシスタンスは診断を提供し、getFunctionCode を使用したコードの改善の提案や getResourceContent に基づくリソース読み込みの最適化など、実行可能な手順を提案できます。

ただし、関連データの取得は課題の半分にすぎません。粒度の細かいデータを提供する関数を使用しても、それらの関数から返されるデータが大量になることがあります。別の例として、getDetailedCallTree は何百ものノードを含むツリーを返すことがあります。標準の JSON では、ネストのためだけに多くの {} が必要になります。

そのため、トークン効率を高めるのに十分な密度がありながら、LLM が理解して参照できるほど構造化された形式が必要です。

データをシリアル化する

この課題にどのように取り組んだかについて、詳しく見ていきましょう。パフォーマンス トレースのデータの大部分を占めるコールツリーの例を引き続き使用します。参考までに、次の例は、JSON のコールスタック内の単一のタスクを示しています。

{
  "id": 2,
  "name": "animate",
  "selected": true,
  "duration": 150,
  "selfTime": 20,
  "children": [3, 5, 6, 7, 10, 11, 12]
}

次のスクリーンショットに示すように、1 つのパフォーマンス トレースに数千個のイベントを含めることができます。小さな色付きのボックスはすべて、このオブジェクト構造で表されます。

DevTools で記録されたパフォーマンス トレースのコールスタック

この形式は DevTools でプログラム的に操作するには適していますが、次の理由により LLM には無駄があります。

  1. 冗長なキー: "duration""selfTime""children" などの文字列が、呼び出しツリーのすべてのノードで繰り返されています。したがって、モデルに送信された 500 個のノードを含むツリーは、これらのキーごとに 500 回トークンを消費します。
  2. 詳細なリスト: children を使用してすべての子 ID を個別にリストすると、特に多くのダウンストリーム イベントをトリガーするタスクでは、大量のトークンが消費されます。

パフォーマンスの AI アシスタンスで使用されるすべてのデータに対して、トークン効率の高い形式を実装するプロセスは段階的に行われました。

最初のイテレーション

パフォーマンスの AI アシスタンスの開発に着手した際、Google はリリース速度を最適化しました。トークンの最適化に対するアプローチは基本的なもので、元の JSON から中かっこやカンマを取り除き、次のような形式にしました。

allUrls = [...]

Node: 1 - update
Selected: false
Duration: 200
Self Time: 50
Children:
   2 - animate

Node: 2 - animate
Selected: true
Duration: 150
Self Time: 20
URL: 0
Children:
   3 - calculatePosition
   5 - applyStyles
   6 - applyStyles
   7 - calculateLayout
   10 - applyStyles
   11 - applyStyles
   12 - applyStyles

Node: 3 - calculatePosition
Selected: false
Duration: 15
Self Time: 2
URL: 0
Children:
   4 - getBoundingClientRect

...

しかし、この最初のバージョンは、生の JSON からのわずかな改善にすぎませんでした。ノードの子は ID と名前で明示的にリストされ、各行の先頭には説明的な繰り返しキー(Node:Selected:Duration: など)が付加されていました。

子ノードのリストを最適化する

最適化をさらに進めるため、ノードの子の名前(前の例の calculatePositionapplyStyles など)を削除しました。AI アシスタンスは関数呼び出しを通じてすべてのノードにアクセスでき、この情報はすでにノードヘッド(Node: 3 - calculatePosition)にあるため、この情報を繰り返す必要はありません。これにより、Children を整数の単純なリストに折りたたむことができました。

Node: 2 - animate
Selected: true
Duration: 150
Self Time: 20
URL: 0
Children: 3, 5, 6, 7, 10, 11, 12

..

これは以前から大幅な改善でしたが、さらに最適化の余地がありました。前の例を見ると、Children はほぼ連続しており、489 のみが欠落していることがわかります。

これは、最初の試行で、深さ優先探索(DFS)アルゴリズムを使用してパフォーマンス トレースからツリーデータをシリアル化していたためです。これにより、兄弟ノードの ID が連続しないため、すべての ID を個別にリストする必要がありました。

幅優先探索(BFS)を使用してツリーのインデックスを再作成すると、連続した ID が取得され、別の最適化が可能になることがわかりました。個々の ID をリストする代わりに、元の例の 3-9 のように、数百もの子を 1 つのコンパクトな範囲で表すことができるようになりました。

最適化された Children リストを含む最終的なノード表記は次のようになります。

allUrls = [...]

Node: 2 - animate
Selected: true
Duration: 150
Self Time: 20
URL: 0
Children: 3-9

キーの数を減らす

ノードリストを最適化した後、冗長なキーの処理に進みました。まず、以前の形式からすべてのキーを削除しました。結果は次のとおりです。

allUrls = [...]

2;animate;150;20;0;3-10

トークン効率は優れていましたが、Gemini にこのデータを理解する方法を指示する必要がありました。そのため、Gemini にコールツリーを初めて送信したときに、次のプロンプトを含めました。

...
Each call frame is presented in the following format:

'id;name;duration;selfTime;urlIndex;childRange;[S]'

Key definitions:

*   id: A unique numerical identifier for the call frame.
*   name: A concise string describing the call frame (e.g., 'Evaluate Script', 'render', 'fetchData').
*   duration: The total execution time of the call frame, including its children.
*   selfTime: The time spent directly within the call frame, excluding its children's execution.
*   urlIndex: Index referencing the "All URLs" list. Empty if no specific script URL is associated.
*   childRange: Specifies the direct children of this node using their IDs. If empty ('' or 'S' at the end), the node has no children. If a single number (e.g., '4'), the node has one child with that ID. If in the format 'firstId-lastId' (e.g., '4-5'), it indicates a consecutive range of child IDs from 'firstId' to 'lastId', inclusive.
*   S: **Optional marker.** The letter 'S' appears at the end of the line **only** for the single call frame selected by the user.

....

この形式の説明にはトークン費用が発生しますが、会話全体に対して 1 回だけ支払われる静的費用です。費用は、以前の最適化で得られた節約額を上回っています。

まとめ

AI を使用して構築する際には、トークンの使用量を最適化することが重要です。生の JSON から専用のカスタム形式に移行し、幅優先探索でツリーのインデックスを再作成し、ツール呼び出しを使用してオンデマンドでデータを取得することで、Chrome DevTools の AI アシスタントが消費するトークンの量を大幅に削減しました。

これらの最適化は、パフォーマンス トレースの AI アシスタンスを有効にするための前提条件でした。コンテキスト ウィンドウが制限されているため、大量のデータを処理できません。ただし、最適化された形式では、ノイズに圧倒されることなく、より長い会話履歴を維持し、より正確でコンテキストを認識した回答を提供できるパフォーマンス エージェントが実現します。

これらの手法が、AI 向けに設計する際に独自のデータ構造を再検討するきっかけになれば幸いです。ウェブ アプリケーションで AI を使用する方法については、web.dev で AI を学ぶをご覧ください。