Progettazione di DevTools: utilizzo efficiente dei token nell'assistenza AI

Data di pubblicazione: 30 gennaio 2026

Durante la creazione dell'assistenza AI per il rendimento, la principale sfida ingegneristica è stata far funzionare Gemini in modo ottimale con le tracce del rendimento registrate in DevTools.

I modelli linguistici di grandi dimensioni (LLM) operano all'interno di una "finestra di contesto", che si riferisce a un limite rigoroso alla quantità di informazioni che possono elaborare contemporaneamente. Questa capacità viene misurata in token. Per i modelli Gemini, un token è un gruppo di circa quattro caratteri.

Le tracce delle prestazioni sono file JSON di grandi dimensioni, spesso costituiti da diversi megabyte. L'invio di una traccia non elaborata esaurirebbe immediatamente la finestra contestuale di un modello e non lascerebbe spazio per le tue domande.

Per rendere possibile l'assistenza dell'AI per il rendimento, abbiamo dovuto progettare un sistema che massimizzasse la quantità di dati utili per un LLM con un utilizzo minimo di token. In questo blog puoi scoprire le tecniche che abbiamo utilizzato per farlo e adottarle per i tuoi progetti.

Personalizzare il contesto iniziale

Il debug del rendimento di un sito web è un'attività complessa. Uno sviluppatore può esaminare l'intera traccia per il contesto, concentrarsi sulle metriche web essenziali e sugli intervalli di tempo correlati della traccia o persino scendere nei dettagli e concentrarsi su singoli eventi come clic o scorrimenti e i relativi stack di chiamate.

Per facilitare il processo di debug, l'assistenza AI di DevTools deve corrispondere a questi percorsi per sviluppatori e funzionare solo con i dati pertinenti per fornire consigli specifici per l'obiettivo dello sviluppatore. Quindi, invece di inviare sempre la traccia completa, abbiamo creato un'assistenza basata sull'AI per suddividere i dati in base all'attività di debug:

Attività di debug Dati inizialmente inviati all'assistenza AI
Chattare di una traccia di rendimento Riepilogo della traccia: un report basato su testo che include informazioni di alto livello dalla sessione di traccia e debug. Include l'URL della pagina, le condizioni di limitazione, le principali metriche di rendimento (LCP, INP, CLS), un elenco di approfondimenti disponibili e, se disponibile, un riepilogo di CrUX.
Chattare di un insight sul rendimento Riepilogo della traccia e il nome dell'approfondimento sul rendimento selezionato.
Chattare di un'attività da una traccia Riepilogo della traccia e albero delle chiamate serializzato in cui si trova l'attività selezionata.
Chatta in merito a una richiesta di rete Riepilogo della traccia e chiave e timestamp della richiesta selezionati
Generare annotazioni di traccia L'albero delle chiamate serializzato in cui si trova l'attività selezionata. L'albero serializzato identifica l'attività selezionata.

Il riepilogo della traccia viene quasi sempre inviato per fornire il contesto iniziale a Gemini, il modello di base dell'assistenza AI. Per le annotazioni create con l'AI, viene omesso.

Fornire strumenti all'AI

L'assistenza AI in DevTools funziona come un agente. Ciò significa che può eseguire autonomamente query per ottenere più dati, in base al prompt iniziale dello sviluppatore e al contesto iniziale condiviso. Per eseguire query su più dati, abbiamo fornito all'assistenza AI un insieme di funzioni predefinite che può chiamare. Un pattern noto come chiamata di funzione o utilizzo di strumenti.

In base ai percorsi di debug descritti in precedenza, abbiamo definito un insieme di funzioni granulari per l'agente. Queste funzioni analizzano in dettaglio gli aspetti considerati importanti in base al contesto iniziale, in modo simile a come un developer umano approccerebbe il debug delle prestazioni. Il set di funzioni è il seguente:

Funzione Descrizione
getInsightDetails(name) Restituisce informazioni dettagliate su un insight sul rendimento specifico (ad esempio,dettagli sul motivo per cui è stato segnalato LCP).
getEventByKey(key) Restituisce le proprietà dettagliate per un singolo evento specifico.
getMainThreadTrackSummary(start, end) Restituisce un riepilogo dell'attività del thread principale per i limiti specificati, inclusi riepiloghi dall'alto verso il basso, dal basso verso l'alto e di terze parti.
getNetworkTrackSummary(start, end) Restituisce un riepilogo dell'attività di rete per i limiti di tempo specificati.
getDetailedCallTree(event_key) Restituisce l'intero albero delle chiamate per un evento del thread principale specifico nella traccia delle prestazioni
getFunctionCode(url, line, col) Restituisce il codice sorgente di una funzione definita in una posizione specifica di una risorsa, annotato con i dati sulle prestazioni di runtime della traccia delle prestazioni
getResourceContent(url) Restituisce i contenuti di una risorsa di testo utilizzata dalla pagina (ad esempio HTML o CSS).

Limitando rigorosamente il recupero dei dati a queste chiamate di funzioni, ci assicuriamo che solo le informazioni pertinenti entrino nella finestra contestuale in un formato ben definito, ottimizzando l'utilizzo dei token.

Esempio di operazione dell'agente

Vediamo un esempio pratico di come l'assistenza AI utilizza la chiamata di funzione per recuperare più informazioni. Dopo una richiesta iniziale di "Perché questa richiesta è lenta?", L'assistenza AI può chiamare le seguenti funzioni in modo incrementale:

  1. getEventByKey: recupera la suddivisione dettagliata dei tempi (TTFB, tempo di download) della richiesta specifica selezionata dall'utente.
  2. getMainThreadTrackSummary: controlla se il thread principale era occupato (bloccato) quando la richiesta avrebbe dovuto iniziare.
  3. getNetworkTrackSummary: analizza se altre risorse competevano per la larghezza di banda nello stesso momento.
  4. getInsightDetails: controlla se il riepilogo della traccia menziona già un approfondimento relativo a questa richiesta come collo di bottiglia.

Combinando i risultati di queste chiamate, l'assistenza AI può quindi fornire una diagnosi e proporre passaggi pratici, ad esempio suggerendo miglioramenti del codice utilizzando getFunctionCode o ottimizzando il caricamento delle risorse in base a getResourceContent.

Tuttavia, il recupero dei dati pertinenti è solo metà della sfida. Anche con le funzioni che forniscono dati granulari, i dati restituiti da queste funzioni possono essere di grandi dimensioni. Per fare un altro esempio, getDetailedCallTree può restituire un albero con centinaia di nodi. Nel formato JSON standard, sarebbero necessari molti { e } solo per la nidificazione.

Pertanto, è necessario un formato sufficientemente denso da essere efficiente in termini di token, ma comunque strutturato in modo che un LLM possa comprenderlo e farvi riferimento.

Serializzare i dati

Vediamo più nel dettaglio come abbiamo affrontato questa sfida, continuando con l'esempio dell'albero delle chiamate, poiché gli alberi delle chiamate costituiscono la maggior parte dei dati in una traccia delle prestazioni. Come riferimento, i seguenti esempi mostrano una singola attività in uno stack di chiamate in formato JSON:

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

Una traccia del rendimento può contenerne migliaia, come mostrato nello screenshot seguente. Ogni piccolo riquadro colorato è rappresentato utilizzando questa struttura dell'oggetto.

Uno stack di chiamate in una traccia delle prestazioni registrata in DevTools

Questo formato è utile per lavorare a livello di programmazione in DevTools, ma è dispendioso per gli LLM per i seguenti motivi:

  1. Chiavi ridondanti:stringhe come "duration", "selfTime" e "children" vengono ripetute per ogni nodo nell'albero delle chiamate. Pertanto, un albero con 500 nodi inviato a un modello consumerebbe token per ciascuna di queste chiavi 500 volte.
  2. Elenchi dettagliati:elencare ogni ID figlio singolarmente tramite children consuma un numero elevato di token, soprattutto per le attività che attivano molti eventi downstream.

L'implementazione di un formato efficiente in termini di token per tutti i dati utilizzati con l'assistenza dell'AI per il rendimento è stato un processo graduale.

Prima iterazione

Quando abbiamo iniziato a lavorare sull'assistenza AI per il rendimento, abbiamo ottimizzato la velocità di spedizione. Il nostro approccio all'ottimizzazione dei token era di base e abbiamo rimosso le parentesi graffe e le virgole dal JSON originale, ottenendo un formato come il seguente:

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

...

Ma questa prima versione era solo un leggero miglioramento rispetto al JSON non elaborato. Elencava ancora esplicitamente i nodi secondari con ID e nomi e anteponeva chiavi descrittive e ripetute (Node:, Selected:, Duration:, …) davanti a ogni riga.

Ottimizzare gli elenchi dei nodi secondari

Come passaggio successivo per un'ulteriore ottimizzazione, abbiamo rimosso i nomi dei nodi secondari (calculatePosition, applyStyles, … nell'esempio precedente). Poiché l'assistenza dell'AI ha accesso a tutti i nodi tramite la chiamata di funzione e queste informazioni sono già nell'intestazione del nodo (Node: 3 - calculatePosition), non è necessario ripeterle. In questo modo, abbiamo potuto comprimere Children in un semplice elenco di numeri interi:

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

..

Sebbene si trattasse di un netto miglioramento rispetto a prima, c'era ancora spazio per l'ottimizzazione. Se esamini l'esempio precedente, potresti notare che Children è quasi sequenziale, con solo 4, 8 e 9 mancanti.

Il motivo è che nel primo tentativo abbiamo utilizzato un algoritmo di ricerca in profondità (DFS) per serializzare i dati dell'albero dalla traccia del rendimento. Di conseguenza, gli ID dei nodi secondari non sono sequenziali, il che ci ha costretto a elencare ogni ID singolarmente.

Ci siamo resi conto che se reindicizzassimo l'albero utilizzando la ricerca in ampiezza (BFS), otterremmo invece ID sequenziali, rendendo possibile un'altra ottimizzazione. Anziché elencare singoli ID, ora possiamo rappresentare anche centinaia di bambini con un unico intervallo compatto, ad esempio 3-9 per l'esempio originale.

La notazione del nodo finale, con l'elenco Children ottimizzato, ha il seguente aspetto:

allUrls = [...]

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

Riduci il numero di chiavi

Con gli elenchi di nodi ottimizzati, siamo passati alle chiavi ridondanti. Abbiamo iniziato rimuovendo tutte le chiavi dal formato precedente, ottenendo questo risultato:

allUrls = [...]

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

Sebbene efficiente in termini di token, dovevamo comunque fornire a Gemini istruzioni su come comprendere questi dati. Di conseguenza, la prima volta che abbiamo inviato un albero delle chiamate a Gemini, abbiamo incluso il seguente prompt:

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

....

Sebbene questa descrizione del formato comporti un costo in token, si tratta di un costo statico pagato una sola volta per l'intera conversazione. Il costo è compensato dai risparmi ottenuti grazie alle ottimizzazioni precedenti.

Conclusione

L'ottimizzazione dell'utilizzo dei token è un aspetto fondamentale da considerare quando si crea con l'AI. Passando dal formato JSON non elaborato a un formato personalizzato specializzato, reindicizzando gli alberi con la ricerca in ampiezza e utilizzando le chiamate di strumenti per recuperare i dati on demand, abbiamo ridotto significativamente la quantità di token consumati dall'assistenza AI in Chrome DevTools.

Queste ottimizzazioni erano un prerequisito per l'attivazione dell'assistenza dell'AI per le tracce di rendimento. A causa della finestra contestuale limitata, altrimenti non potrebbe gestire l'enorme volume di dati. Tuttavia, il formato ottimizzato consente a un agente che può mantenere una cronologia delle conversazioni più lunga e fornire risposte più accurate e sensibili al contesto senza essere sopraffatto dal rumore.

Ci auguriamo che queste tecniche ti ispirino a dare un'occhiata più da vicino alle tue strutture di dati quando progetti per l'AI. Per iniziare a utilizzare l'AI nelle applicazioni web, esplora Learn AI su web.dev.