Entwicklung von Entwicklertools: Effiziente Tokennutzung bei KI-Unterstützung

Veröffentlicht am 30. Januar 2026

Bei der Entwicklung der KI-Unterstützung für die Leistung bestand die größte technische Herausforderung darin, Gemini mit Leistungs-Traces, die in DevTools aufgezeichnet wurden, kompatibel zu machen.

Large Language Models (LLMs) arbeiten innerhalb eines „Kontextfensters“, das sich auf eine strenge Begrenzung der Menge an Informationen bezieht, die sie gleichzeitig verarbeiten können. Diese Kapazität wird in Tokens gemessen. Bei Gemini-Modellen entspricht ein Token etwa einer Gruppe von vier Zeichen.

Leistungs-Traces sind große JSON-Dateien, die oft mehrere Megabyte umfassen. Wenn Sie einen Roh-Trace senden, wird das Kontextfenster eines Modells sofort ausgeschöpft und es bleibt kein Platz für Ihre Fragen.

Damit KI-Unterstützung für Performance möglich ist, mussten wir ein System entwickeln, das die Menge an nützlichen Daten für ein LLM bei minimaler Tokennutzung maximiert. In diesem Blogbeitrag erfahren Sie mehr über die Techniken, die wir dafür verwendet haben, und können sie für Ihre eigenen Projekte übernehmen.

Anfangskontext anpassen

Die Leistung einer Website zu debuggen ist eine komplexe Aufgabe. Ein Entwickler kann sich entweder den vollständigen Trace ansehen, um den Kontext zu erfassen, sich auf die Core Web Vitals und die zugehörigen Zeiträume des Traces konzentrieren oder sogar bis ins Detail gehen und sich auf einzelne Ereignisse wie Klicks oder Scrolls und die zugehörigen Callstacks konzentrieren.

Um den Debugging-Prozess zu unterstützen, muss die KI-Unterstützung von DevTools diesen Entwicklerpfaden entsprechen und nur mit den relevanten Daten arbeiten, um Ratschläge zu geben, die auf den Fokus des Entwicklers zugeschnitten sind. Anstatt immer den vollständigen Trace zu senden, haben wir KI-Unterstützung entwickelt, um die Daten basierend auf Ihrer Debugging-Aufgabe aufzuteilen:

Debugging-Aufgabe Daten, die ursprünglich an die KI-Unterstützung gesendet wurden
Über einen Leistungstrace chatten Zusammenfassung des Traces: Ein textbasierter Bericht, der allgemeine Informationen aus dem Trace und der Debugging-Sitzung enthält. Enthält die Seiten-URL, Drosselungsbedingungen, wichtige Leistungsmesswerte (LCP, INP, CLS), eine Liste der verfügbaren Statistiken und, falls verfügbar, eine CrUX-Zusammenfassung.
Über einen Leistungs-Insight chatten Zusammenfassung des Traces und Name des ausgewählten Leistungs-Insights.
Über eine Aufgabe aus einem Trace chatten Die Zusammenfassung des Traces und der serialisierte Aufrufbaum, in dem sich die ausgewählte Aufgabe befindet.
Über eine Netzwerkanfrage chatten Zusammenfassung des Traces sowie der ausgewählte Anfrageschlüssel und Zeitstempel
Trace-Annotationen generieren Der serialisierte Aufrufbaum, in dem sich die ausgewählte Aufgabe befindet. Der serialisierte Baum gibt an, welche Aufgabe ausgewählt ist.

Die Zusammenfassung des Traces wird fast immer gesendet, um Gemini, dem zugrunde liegenden Modell der KI-Unterstützung, einen ersten Kontext zu liefern. Bei KI-generierten Anmerkungen wird sie weggelassen.

Tools für KI bereitstellen

Die KI-Unterstützung in den Entwicklertools funktioniert als Agent. Das bedeutet, dass sie basierend auf dem ursprünglichen Prompt des Entwicklers und dem ursprünglichen Kontext, der mit ihr geteilt wurde, autonom weitere Daten abfragen kann. Um mehr Daten abzufragen, haben wir der KI-Unterstützung eine Reihe vordefinierter Funktionen zur Verfügung gestellt, die sie aufrufen kann. Ein Muster, das als Funktionsaufruf oder Tool-Nutzung bezeichnet wird.

Basierend auf den oben beschriebenen Debugging-Prozessen haben wir eine Reihe von detaillierten Funktionen für den Agenten definiert. Diese Funktionen gehen auf Details ein, die basierend auf dem ursprünglichen Kontext als wichtig erachtet werden. Das ist ähnlich wie bei der Leistungsfehlersuche durch einen menschlichen Entwickler. Die Funktionen sind:

Funktion Beschreibung
getInsightDetails(name) Gibt detaillierte Informationen zu einem bestimmten Leistungs-Insight zurück, z. B. Details dazu, warum LCP gekennzeichnet wurde.
getEventByKey(key) Gibt detaillierte Eigenschaften für ein einzelnes, bestimmtes Ereignis zurück.
getMainThreadTrackSummary(start, end) Gibt eine Zusammenfassung der Hauptthread-Aktivität für die angegebenen Grenzen zurück, einschließlich Top-down-, Bottom-up- und Drittanbieterzusammenfassungen.
getNetworkTrackSummary(start, end) Gibt eine Zusammenfassung der Netzwerkaktivität für den angegebenen Zeitraum zurück.
getDetailedCallTree(event_key) Gibt den vollständigen Aufrufbaum für ein bestimmtes Hauptthread-Ereignis im Leistungstrace zurück.
getFunctionCode(url, line, col) Gibt den Quellcode für eine Funktion zurück, die an einem bestimmten Speicherort in einer Ressource definiert ist. Der Quellcode ist mit Laufzeitleistungsdaten aus dem Leistungstrace annotiert.
getResourceContent(url) Gibt den Inhalt einer von der Seite verwendeten Textressource zurück, z. B. HTML oder CSS.

Indem wir den Datenabruf streng auf diese Funktionsaufrufe beschränken, sorgen wir dafür, dass nur relevante Informationen in einem genau definierten Format in das Kontextfenster gelangen, wodurch die Tokennutzung optimiert wird.

Beispiel für einen Agent-Vorgang

Sehen wir uns ein praktisches Beispiel dafür an, wie die KI-Unterstützung Funktionsaufrufe verwendet, um weitere Informationen abzurufen. Nach dem ersten Prompt „Warum dauert diese Anfrage so lange?“ Die KI-Unterstützung kann die folgenden Funktionen inkrementell aufrufen:

  1. getEventByKey: Ruft die detaillierte Zeitaufschlüsselung (TTFB, Downloadzeit) der vom Nutzer ausgewählten Anfrage ab.
  2. getMainThreadTrackSummary: Prüfen Sie, ob der Hauptthread beschäftigt (blockiert) war, als die Anfrage hätte gestartet werden sollen.
  3. getNetworkTrackSummary: Analysieren Sie, ob andere Ressourcen gleichzeitig um Bandbreite konkurriert haben.
  4. getInsightDetails: Prüfen Sie, ob in der Trace-Zusammenfassung bereits ein Hinweis zu dieser Anfrage als Engpass enthalten ist.

Durch die Kombination der Ergebnisse dieser Aufrufe kann die KI-Unterstützung dann eine Diagnose erstellen und umsetzbare Schritte vorschlagen, z. B. Codeverbesserungen mit getFunctionCode oder die Optimierung des Ressourcenladens basierend auf getResourceContent.

Das Abrufen relevanter Daten ist jedoch nur die halbe Miete. Auch wenn Funktionen detaillierte Daten liefern, kann die von ihnen zurückgegebene Datenmenge sehr groß sein. Ein weiteres Beispiel: getDetailedCallTree kann einen Baum mit Hunderten von Knoten zurückgeben. In Standard-JSON wären das viele { und } nur für die Verschachtelung.

Daher ist ein Format erforderlich, das dicht genug ist, um token-effizient zu sein, aber dennoch strukturiert genug, damit ein LLM es verstehen und darauf verweisen kann.

Daten serialisieren

Sehen wir uns genauer an, wie wir diese Herausforderung angegangen sind. Wir verwenden dazu weiterhin das Beispiel des Aufrufbaums, da Aufrufbäume den Großteil der Daten in einem Leistungstrace ausmachen. Die folgenden Beispiele zeigen eine einzelne Aufgabe in einem Aufrufstapel in JSON:

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

Ein Leistungstrace kann Tausende davon enthalten, wie im folgenden Screenshot zu sehen ist. Jedes kleine farbige Kästchen wird mit dieser Objektstruktur dargestellt.

Ein Callstack in einem aufgezeichneten Leistungs-Trace in DevTools

Dieses Format eignet sich gut für die programmatische Arbeit in den DevTools, ist aber aus folgenden Gründen für LLMs ineffizient:

  1. Redundante Schlüssel:Strings wie "duration", "selfTime" und "children" werden für jeden einzelnen Knoten im Aufrufbaum wiederholt. Wenn also ein Baum mit 500 Knoten an ein Modell gesendet wird, werden für jeden dieser Schlüssel 500 Tokens verbraucht.
  2. Ausführliche Listen:Wenn Sie jede untergeordnete ID einzeln über children auflisten, werden sehr viele Tokens verbraucht, insbesondere bei Aufgaben, die viele Downstream-Ereignisse auslösen.

Die Implementierung eines token-effizienten Formats für alle Daten, die mit KI-Unterstützung für Performance verwendet werden, war ein schrittweiser Prozess.

Erste Iteration

Als wir mit der Entwicklung der KI-Unterstützung für Performance begonnen haben, haben wir die Versandgeschwindigkeit optimiert. Unser Ansatz zur Token-Optimierung war einfach. Wir haben die ursprüngliche JSON-Datei von geschweiften Klammern und Kommas befreit, was zu einem Format wie dem folgenden geführt hat:

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

...

Diese erste Version war jedoch nur eine geringfügige Verbesserung gegenüber dem reinen JSON. Die untergeordneten Knoten mit IDs und Namen wurden weiterhin explizit aufgeführt und jeder Zeile wurden beschreibende, wiederholte Schlüssel (Node:, Selected:, Duration: usw.) vorangestellt.

Listen mit untergeordneten Knoten optimieren

Als Nächstes haben wir die Namen für untergeordnete Knoten entfernt (calculatePosition, applyStyles usw. im vorherigen Beispiel), um die Optimierung weiter zu verbessern. Da KI-Unterstützung über Funktionsaufrufe auf alle Knoten zugreifen kann und diese Informationen bereits im Knoten-Head (Node: 3 - calculatePosition) enthalten sind, müssen sie nicht wiederholt werden., So konnten wir Children auf eine einfache Liste von Ganzzahlen reduzieren:

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

..

Das war zwar eine deutliche Verbesserung, aber es gab noch Optimierungspotenzial. Wenn Sie sich das vorherige Beispiel ansehen, werden Sie feststellen, dass Children fast sequenziell ist. Es fehlen nur 4, 8 und 9.

Das liegt daran, dass wir bei unserem ersten Versuch einen Tiefensuche-Algorithmus (Depth-First Search, DFS) verwendet haben, um Baumdaten aus dem Leistungstrace zu serialisieren. Das führte zu nicht sequenziellen IDs für untergeordnete Knoten, sodass wir jede ID einzeln auflisten mussten.

Wir haben festgestellt, dass wir durch die Neuindexierung des Baums mit Breadth-First Search (BFS) stattdessen sequenzielle IDs erhalten würden, was eine weitere Optimierung ermöglicht. Anstatt einzelne IDs aufzulisten, können wir jetzt auch Hunderte von untergeordneten Elementen mit einem einzigen kompakten Bereich darstellen, z. B. 3-9 für das ursprüngliche Beispiel.

Die endgültige Knotenschreibweise mit der optimierten Children-Liste sieht so aus:

allUrls = [...]

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

Anzahl der Schlüssel reduzieren

Nachdem wir die Knotenlisten optimiert hatten, wandten wir uns den redundanten Schlüsseln zu. Zuerst haben wir alle Schlüssel aus dem vorherigen Format entfernt. Das Ergebnis:

allUrls = [...]

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

Das ist zwar token-effizient, aber wir mussten Gemini trotzdem Anweisungen geben, wie diese Daten zu interpretieren sind. Als wir das erste Mal einen Aufrufbaum an Gemini gesendet haben, haben wir den folgenden Prompt verwendet:

...
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.

....

Für diese Formatbeschreibung fallen zwar Token-Kosten an, aber es handelt sich um statische Kosten, die nur einmal für die gesamte Unterhaltung bezahlt werden. Die Kosten werden durch die Einsparungen, die durch die vorherigen Optimierungen erzielt wurden, mehr als ausgeglichen.

Fazit

Die Optimierung der Token-Nutzung ist ein wichtiger Aspekt bei der Entwicklung mit KI. Durch die Umstellung von rohem JSON auf ein spezielles benutzerdefiniertes Format, die Neuindexierung von Bäumen mit der Breitensuche und die Verwendung von Tool-Aufrufen zum Abrufen von Daten bei Bedarf haben wir die Anzahl der Tokens, die von der KI-Unterstützung in den Chrome-Entwicklertools verwendet werden, erheblich reduziert.

Diese Optimierungen waren eine Voraussetzung für die KI-Unterstützung bei Leistungstraces. Aufgrund des begrenzten Kontextfensters könnte es sonst die schiere Menge an Daten nicht verarbeiten. Das optimierte Format ermöglicht jedoch einen Leistungsagenten, der einen längeren Unterhaltungsverlauf aufrechterhalten und genauere, kontextbezogene Antworten liefern kann, ohne durch Rauschen überfordert zu werden.

Wir hoffen, dass diese Techniken Sie dazu anregen, Ihre eigenen Datenstrukturen bei der Entwicklung für KI noch einmal zu überdenken. Auf web.dev finden Sie Informationen zum Einstieg in KI in Webanwendungen.