1. Introduzione
Questo è un codelab interattivo per imparare a misurare l'Interaction to Next Paint (INP) utilizzando la libreria web-vitals
.
Prerequisiti
- Conoscenza dello sviluppo di HTML e JavaScript.
- Consigliato: leggi la documentazione relativa alle metriche INP di web.dev.
Cosa imparerai a fare
- Come aggiungere la libreria
web-vitals
alla tua pagina e utilizzare i relativi dati di attribuzione. - Utilizza i dati di attribuzione per diagnosticare dove e come iniziare a migliorare l'INP.
Cosa ti serve
- Un computer in grado di clonare il codice da GitHub ed eseguire i comandi npm.
- Un editor di testo.
- Una versione recente di Chrome per il funzionamento di tutte le misurazioni delle interazioni.
2. Configurazione
Recupera ed esegui il codice
Il codice si trova nel repository web-vitals-codelabs
.
- Clona il repository nel terminale:
git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git
. - Esplora la directory clonata:
cd web-vitals-codelabs/measuring-inp
. - Installa le dipendenze:
npm ci
. - Avvia il server web:
npm run start
. - Visita la pagina http://localhost:8080/ nel browser.
Prova la pagina
Questo codelab utilizza Gastropodicon (un popolare sito di riferimento sull'anatomia delle lumache) per esplorare potenziali problemi con l'INP.
Prova a interagire con la pagina per capire quali interazioni sono lente.
3. Orientarsi in Chrome DevTools
Apri DevTools dal menu Altri strumenti > Strumenti per sviluppatori, facendo clic con il tasto destro del mouse sulla pagina e selezionando Ispeziona o utilizzando una scorciatoia da tastiera.
In questo codelab utilizzeremo sia il riquadro Rendimento sia la console. Puoi passare da una all'altra in qualsiasi momento nelle schede nella parte superiore di DevTools.
- I problemi INP si verificano più spesso sui dispositivi mobili, quindi passa all'emulazione del display mobile.
- Se esegui il test su un computer o un laptop, le prestazioni saranno probabilmente molto migliori rispetto a un dispositivo mobile reale. Per un'analisi più realistica del rendimento, fai clic sull'ingranaggio in alto a destra nel riquadro Rendimento e seleziona Rallentamento della CPU 4 volte.
4. Installazione di web-vitals
web-vitals
è una libreria JavaScript per la misurazione delle metriche Web Vitals che gli utenti utilizzano. Puoi utilizzare la libreria per acquisire questi valori e poi inviarli a un endpoint di analisi per un'analisi successiva, per capire quando e dove si verificano interazioni lente.
Esistono diversi modi per aggiungere la raccolta a una pagina. La modalità di installazione della libreria sul tuo sito dipende dalla gestione delle dipendenze, dal processo di compilazione e da altri fattori. Assicurati di consultare la documentazione della libreria per tutte le opzioni disponibili.
Questo codelab verrà installato da npm e caricherà lo script direttamente per evitare di esaminare una determinata procedura di compilazione.
Puoi utilizzare due versioni di web-vitals
:
- La build "standard" deve essere utilizzata se vuoi monitorare i valori delle metriche di Core Web Vitals al caricamento di una pagina.
- La compilazione "attribution" aggiunge ulteriori informazioni di debug a ogni metrica per diagnosticare il motivo per cui una metrica ha il valore che ha.
Per misurare l'INP in questo codelab, è necessaria la compilazione dell'attribuzione.
Aggiungi web-vitals
a devDependencies
del progetto eseguendo npm install -D web-vitals
Aggiungi web-vitals
alla pagina:
Aggiungi la versione di attribuzione dello script alla fine di index.html
e registra i risultati nella console:
<script type="module">
import {onINP} from './node_modules/web-vitals/dist/web-vitals.attribution.js';
onINP(console.log);
</script>
Prova
Prova a interagire di nuovo con la pagina con la console aperta. Quando fai clic sulla pagina, non viene registrato nulla.
L'INP viene misurato durante l'intero ciclo di vita di una pagina, pertanto per impostazione predefinita web-vitals
non lo registra finché l'utente non esce o chiude la pagina. Questo è il comportamento ideale per i beacon per qualcosa come gli analytics, ma è meno ideale per il debug interattivo.
web-vitals
fornisce un'opzione reportAllChanges
per report più dettagliati. Se questa opzione è attivata, non viene registrata ogni interazione, ma ogni volta che si verifica un'interazione più lenta di quella precedente.
Prova ad aggiungere l'opzione allo script e a interagire di nuovo con la pagina:
<script type="module">
import {onINP} from './node_modules/web-vitals/dist/web-vitals.attribution.js';
onINP(console.log, {reportAllChanges: true});
</script>
Aggiorna la pagina e le interazioni dovrebbero ora essere registrate nella console, aggiornandosi ogni volta che viene registrata una nuova interazione più lenta. Ad esempio, prova a digitare nella casella di ricerca ed elimina l'input.
5. Che cos'è un'attribuzione?
Iniziamo con la prima interazione che la maggior parte degli utenti avrà con la pagina, la finestra di dialogo per il consenso all'uso dei cookie.
Molte pagine contengono script che richiedono l'attivazione dei cookie in modo sincrono quando vengono accettati da un utente, il che rende il clic un'interazione lenta. È quello che succede qui.
Fai clic su Sì per accettare i cookie (demo) e dare un'occhiata ai dati INP ora registrati nella console DevTools.
Queste informazioni di primo livello sono disponibili sia nelle build standard che in quelle di attribuzione di Web Vitals:
{
name: 'INP',
value: 344,
rating: 'needs-improvement',
entries: [...],
id: 'v4-1715732159298-8028729544485',
navigationType: 'reload',
attribution: {...},
}
Il periodo di tempo che è iniziato dal momento in cui l'utente ha fatto clic fino al successivo aggiornamento della pagina è stato di 344 millisecondi, un INP "da migliorare". L'array entries
contiene tutti i valori PerformanceEntry
associati a questa interazione, in questo caso un solo evento di clic.
Tuttavia, per capire cosa sta succedendo in questo momento, ci interessano soprattutto le proprietà attribution
. Per creare i dati di attribuzione, web-vitals
individua il Long Animations Frame (LoAF) che si sovrappone all'evento di clic. LoAF può quindi fornire dati dettagliati sul tempo trascorso durante quel frame, dagli script eseguiti al tempo trascorso in un callback, uno stile e un layout requestAnimationFrame
.
Espandi la proprietà attribution
per visualizzare ulteriori informazioni. I dati sono molto più completi.
attribution: {
interactionTargetElement: Element,
interactionTarget: '#confirm',
interactionType: 'pointer',
inputDelay: 27,
processingDuration: 295.6,
presentationDelay: 21.4,
processedEventEntries: [...],
longAnimationFrameEntries: [...],
}
Innanzitutto, ci sono informazioni su ciò con cui è stata eseguita l'interazione:
interactionTargetElement
: un riferimento in tempo reale all'elemento con cui è stata eseguita l'interazione (se l'elemento non è stato rimosso dal DOM).interactionTarget
: un selettore per trovare l'elemento all'interno della pagina.
Successivamente, la tempistica viene suddivisa in modo generale:
inputDelay
: il tempo che intercorre tra l'inizio dell'interazione da parte dell'utente (ad esempio, un clic del mouse) e l'inizio dell'esecuzione del listener di eventi per quell'interazione. In questo caso, il ritardo di input era di soli 27 millisecondi, anche con il throttling della CPU attivo.processingDuration
: il tempo necessario per l'esecuzione completa dei listener di eventi. Spesso le pagine hanno più listener per un singolo evento (ad esempiopointerdown
,pointerup
eclick
). Se vengono eseguiti tutti nello stesso frame di animazione, verranno uniti in questo momento. In questo caso, la durata dell'elaborazione è di 295,6 millisecondi, la maggior parte del tempo INP.presentationDelay
: il tempo che intercorre tra il completamento dei listener di eventi e il termine della visualizzazione del frame successivo da parte del browser. In questo caso, 21,4 millisecondi.
Queste fasi INP possono essere un indicatore fondamentale per diagnosticare ciò che deve essere ottimizzato. La guida all'ottimizzazione di INP contiene ulteriori informazioni su questo argomento.
Se esaminiamo più da vicino, processedEventEntries
contiene cinque eventi, rispetto al singolo evento nell'array entries
INP di primo livello. Qual è la differenza?
processedEventEntries: [
{
name: 'mouseover',
entryType: 'event',
startTime: 1801.6,
duration: 344,
processingStart: 1825.3,
processingEnd: 1825.3,
cancelable: true
},
{
name: 'mousedown',
entryType: 'event',
startTime: 1801.6,
duration: 344,
processingStart: 1825.3,
processingEnd: 1825.3,
cancelable: true
},
{name: 'mousedown', ...},
{name: 'mouseup', ...},
{name: 'click', ...},
],
La voce di primo livello è l' evento INP, in questo caso un clic. L'attribuzione processedEventEntries
sono tutti gli eventi elaborati nello stesso frame. Tieni presente che include altri eventi come mouseover
e mousedown
, non solo l'evento di clic. La conoscenza di questi altri eventi può essere fondamentale se anche loro erano lenti, poiché hanno contribuito a rallentare la reattività.
Infine, c'è l'array longAnimationFrameEntries
. Può trattarsi di una singola voce, ma in alcuni casi un'interazione può estendersi su più frame. Qui abbiamo il caso più semplice con un singolo frame di animazione lungo.
longAnimationFrameEntries
Espansione della voce LoAF:
longAnimationFrameEntries: [{
name: 'long-animation-frame',
startTime: 1823,
duration: 319,
renderStart: 2139.5,
styleAndLayoutStart: 2139.7,
firstUIEventTimestamp: 1801.6,
blockingDuration: 268,
scripts: [{...}]
}],
Sono disponibili una serie di valori utili, ad esempio la suddivisione del tempo impiegato per lo stile. L'articolo sull'API Long Animation Frames fornisce informazioni più dettagliate su queste proprietà. Al momento ci interessa principalmente la proprietà scripts
, che contiene voci che forniscono dettagli sugli script responsabili del frame a lungo termine:
scripts: [{
name: 'script',
invoker: 'BUTTON#confirm.onclick',
invokerType: 'event-listener',
startTime: 1828.6,
executionStart: 1828.6,
duration: 294,
sourceURL: 'http://localhost:8080/third-party/cmp.js',
sourceFunctionName: '',
sourceCharPosition: 1144
}]
In questo caso, possiamo dire che il tempo è stato speso principalmente in un singolo event-listener
, invocato su BUTTON#confirm.onclick
. Possiamo persino vedere l'URL del codice sorgente dello script e la posizione del carattere in cui è stata definita la funzione.
Concetti chiave
Cosa si può stabilire in merito a questa richiesta in base a questi dati di attribuzione?
- L'interazione è stata attivata da un clic sull'elemento
button#confirm
(daattribution.interactionTarget
e dalla proprietàinvoker
in una voce di attribuzione dello script). - Il tempo è stato impiegato principalmente per l'esecuzione degli ascoltatori di eventi (da
attribution.processingDuration
rispetto alla metrica totalevalue
). - Il codice del listener di eventi lento inizia da un listener di clic definito in
third-party/cmp.js
(dascripts.sourceURL
).
Abbiamo dati sufficienti per sapere dove dobbiamo ottimizzare.
6. Più listener di eventi
Aggiorna la pagina in modo che la console DevTools sia vuota e l'interazione per il consenso ai cookie non sia più la più lunga.
Inizia a digitare nella casella di ricerca. Che cosa mostrano i dati di attribuzione? Cosa pensi che stia succedendo?
Dati di attribuzione
Innanzitutto, una scansione di alto livello di un esempio di test della demo:
{
name: 'INP',
value: 1072,
rating: 'poor',
attribution: {
interactionTargetElement: Element,
interactionTarget: '#search-terms',
interactionType: 'keyboard',
inputDelay: 3.3,
processingDuration: 1060.6,
presentationDelay: 8.1,
processedEventEntries: [...],
longAnimationFrameEntries: [...],
}
}
Si tratta di un valore INP scadente (con il throttling della CPU abilitato) derivante da un'interazione con la tastiera con l'elemento input#search-terms
. La maggior parte del tempo, ovvero 1061 millisecondi su un INP totale di 1072 millisecondi, è stata impiegata per la durata dell'elaborazione.
Tuttavia, le voci scripts
sono più interessanti.
Thrashing del layout
La prima voce dell'array scripts
ci fornisce un contesto utile:
scripts: [{
name: 'script',
invoker: 'BUTTON#confirm.onclick',
invokerType: 'event-listener',
startTime: 4875.6,
executionStart: 4875.6,
duration: 497,
forcedStyleAndLayoutDuration: 388,
sourceURL: 'http://localhost:8080/js/index.js',
sourceFunctionName: 'handleSearch',
sourceCharPosition: 940
},
...]
La maggior parte della durata dell'elaborazione si verifica durante l'esecuzione di questo script, che è un ascoltatore input
(l'invoker è INPUT#search-terms.oninput
). Il nome della funzione è specificato (handleSearch
), così come la posizione del carattere all'interno del file di origine index.js
.
Tuttavia, esiste una nuova proprietà: forcedStyleAndLayoutDuration
. Si tratta del tempo trascorso durante l'invocazione di questo script in cui il browser è stato costretto a riorganizzare la pagina. In altre parole, il 78% del tempo (388 millisecondi su 497) impiegato per l'esecuzione di questo gestore di eventi è stato effettivamente speso per il thrashing del layout.
Questo dovrebbe essere un problema da risolvere con la massima priorità.
Ascoltatori ripetuti
Prese singolarmente, le due voci di script che seguono non hanno nulla di particolarmente degno di nota:
scripts: [...,
{
name: 'script',
invoker: '#document.onkeyup',
invokerType: 'event-listener',
startTime: 5375.3,
executionStart: 5375.3,
duration: 124,
sourceURL: 'http://localhost:8080/js/index.js',
sourceFunctionName: '',
sourceCharPosition: 1526,
},
{
name: 'script',
invoker: '#document.onkeyup',
invokerType: 'event-listener',
startTime: 5673.9,
executionStart: 5673.9,
duration: 95,
sourceURL: 'http://localhost:8080/js/index.js',
sourceFunctionName: '',
sourceCharPosition: 1526
}]
Entrambe le voci sono ascoltatori keyup
, che vengono eseguite una dopo l'altra. Gli ascoltatori sono funzioni anonime (quindi non viene riportato nulla nella proprietà sourceFunctionName
), ma abbiamo comunque un file di origine e la posizione del carattere, quindi possiamo trovare dove si trova il codice.
La cosa strana è che entrambi provengono dallo stesso file di origine e dalla stessa posizione del carattere.
Il browser ha finito per elaborare più pressioni dei tasti in un singolo frame di animazione, il che ha portato all'esecuzione di questo gestore di eventi due volte prima che potesse essere visualizzato qualcosa.
Questo effetto può anche essere cumulativo: più tempo impiegano gli ascoltatori di eventi per completare l'operazione, più eventi di input aggiuntivi possono essere inviati, prolungando l'interazione lenta per molto più tempo.
Poiché si tratta di un'interazione di ricerca/completamento automatico, sarebbe una buona strategia eliminare il ritardo dell'input in modo da elaborare al massimo una pressione del tasto per frame.
7. Ritardo di input
Il motivo tipico dei ritardi di input, ovvero il tempo che intercorre tra l'interazione dell'utente e il momento in cui un ascoltatore di eventi può iniziare a elaborare l'interazione, è che il thread principale è occupato. Ciò potrebbe avere più cause:
- La pagina è in fase di caricamento e il thread principale è impegnato a svolgere il lavoro iniziale di configurazione del DOM, di impaginazione e applicazione di stili alla pagina, nonché di valutazione ed esecuzione degli script.
- La pagina è generalmente occupata, ad esempio perché sono in esecuzione calcoli, animazioni basate su script o annunci.
- L'elaborazione delle interazioni precedenti richiede così tanto tempo da ritardare le interazioni future, come si è visto nell'ultimo esempio.
La pagina di demo ha una funzionalità segreta: se fai clic sul logo della lumaca nella parte superiore della pagina, inizierà a animarsi ed eseguirà alcuni compiti JavaScript pesanti nel thread principale.
- Fai clic sul logo a forma di lumaca per avviare l'animazione.
- Le attività JavaScript vengono attivate quando la lumaca si trova nella parte inferiore del rimbalzo. Prova a interagire con la pagina il più vicino possibile al limite inferiore del tasso di abbandono e scopri quanto alto puoi attivare l'INP.
Ad esempio, anche se non attivi altri ascoltatori di eventi, ad esempio facendo clic e attivando lo stato attivo nella casella di ricerca proprio quando la lumaca rimbalza, il lavoro del thread principale causerà un blocco della pagina per un periodo di tempo significativo.
In molte pagine, il lavoro pesante del thread principale non sarà così ben comportato, ma questa è una buona dimostrazione di come può essere identificabile nei dati di attribuzione INP.
Ecco un esempio di attribuzione che si basa solo sull'attenzione alla casella di ricerca durante il bounce a lumaca:
{
name: 'INP',
value: 728,
rating: 'poor',
attribution: {
interactionTargetElement: Element,
interactionTarget: '#search-terms',
interactionType: 'pointer',
inputDelay: 702.3,
processingDuration: 4.9,
presentationDelay: 20.8,
longAnimationFrameEntries: [{
name: 'long-animation-frame',
startTime: 2064.8,
duration: 790,
renderStart: 2065,
styleAndLayoutStart: 2854.2,
firstUIEventTimestamp: 0,
blockingDuration: 740,
scripts: [{...}]
}]
}
}
Come previsto, gli ascoltatori di eventi sono stati eseguiti rapidamente, con una durata di elaborazione di 4,9 millisecondi e la maggior parte del tempo di interazione sfavorevole è stato speso per il ritardo di inserimento, pari a 702,3 millisecondi su un totale di 728.
Questa situazione può essere difficile da risolvere. Anche se sappiamo con cosa e in che modo l'utente ha interagito, sappiamo anche che questa parte dell'interazione è stata completata rapidamente e non ha riscontrato problemi. In realtà, era un altro elemento della pagina a ritardare l'inizio dell'elaborazione dell'interazione, ma come faremmo a sapere da dove iniziare a cercare?
Le voci dello script LoAF sono qui per salvarti:
scripts: [{
name: 'script',
invoker: 'SPAN.onanimationiteration',
invokerType: 'event-listener',
startTime: 2065,
executionStart: 2065,
duration: 788,
sourceURL: 'http://localhost:8080/js/index.js',
sourceFunctionName: 'cryptodaphneCoinHandler',
sourceCharPosition: 1831
}]
Anche se questa funzione non aveva nulla a che fare con l'interazione, ha rallentato il frame dell'animazione ed è quindi inclusa nei dati LoAF uniti all'evento di interazione.
Da ciò possiamo vedere come è stata attivata la funzione che ha ritardato l'elaborazione dell'interazione (da un ascoltatore animationiteration
), esattamente quale funzione era responsabile e dove si trovava nei nostri file di origine.
8. Ritardo nella visualizzazione: quando un aggiornamento non viene visualizzato
Il ritardo di visualizzazione misura il tempo che intercorre tra il termine dell'esecuzione degli ascoltatori di eventi e il momento in cui il browser è in grado di disegnare un nuovo frame sullo schermo, mostrando il feedback visibile all'utente.
Aggiorna la pagina per reimpostare il valore INP, quindi apri il menu di navigazione. C'è un problema quando si apre.
Ecco un esempio
{
name: 'INP',
value: 376,
rating: 'needs-improvement',
delta: 352,
attribution: {
interactionTarget: '#sidenav-button>svg',
interactionType: 'pointer',
inputDelay: 12.8,
processingDuration: 14.7,
presentationDelay: 348.5,
longAnimationFrameEntries: [{
name: 'long-animation-frame',
startTime: 651,
duration: 365,
renderStart: 673.2,
styleAndLayoutStart: 1004.3,
firstUIEventTimestamp: 138.6,
blockingDuration: 315,
scripts: [{...}]
}]
}
}
Questa volta è il ritardo di visualizzazione a costituire la maggior parte dell'interazione lenta. Ciò significa che ciò che blocca il thread principale si verifica dopo il completamento degli ascoltatori di eventi.
scripts: [{
entryType: 'script',
invoker: 'FrameRequestCallback',
invokerType: 'user-callback',
startTime: 673.8,
executionStart: 673.8,
duration: 330,
sourceURL: 'http://localhost:8080/js/side-nav.js',
sourceFunctionName: '',
sourceCharPosition: 1193,
}]
Esaminando la singola voce nell'array scripts
, vediamo che il tempo viene trascorso in un user-callback
da un FrameRequestCallback
. Questa volta il ritardo nella visualizzazione è causato da un callback requestAnimationFrame
.
9. Conclusione
Aggregare i dati sul campo
È opportuno riconoscere che tutto è più facile quando si esamina una singola voce di attribuzione INP da un singolo caricamento di pagina. In che modo questi dati possono essere aggregati per eseguire il debug dell'INP in base ai dati sul campo? La quantità di dettagli utili rende questa operazione più difficile.
Ad esempio, è estremamente utile sapere quale elemento della pagina è una fonte comune di interazioni lente. Tuttavia, se la pagina ha nomi di classi CSS compilati che cambiano da una build all'altra, i selettori web-vitals
dello stesso elemento potrebbero essere diversi nelle varie build.
Devi invece pensare alla tua applicazione specifica per determinare cosa è più utile e come possono essere aggregati i dati. Ad esempio, prima di restituire i dati di attribuzione tramite beacon, puoi sostituire il selettore web-vitals
con un identificatore personale, in base al componente in cui si trova il target o ai ruoli ARIA che soddisfa.
Analogamente, le voci scripts
potrebbero avere hash basati su file nei percorsi sourceURL
che rendono difficile la loro combinazione, ma puoi rimuovere gli hash in base alla procedura di compilazione nota prima di inviare nuovamente i dati.
Purtroppo, non esiste un percorso facile con dati così complessi, ma anche l'utilizzo di un sottoinsieme è più utile rispetto all'assenza di dati sull'attribuzione per il processo di debug.
Attribuzione ovunque.
L'attribuzione INP basata su LoAF è un potente strumento di debug. Offre dati granulari su cosa è successo nello specifico durante un INP. In molti casi, può indicare la posizione esatta in uno script in cui iniziare le attività di ottimizzazione.
Ora puoi utilizzare i dati di attribuzione INP su qualsiasi sito.
Anche se non hai accesso in modifica a una pagina, puoi ricreare la procedura di questo codelab eseguendo il seguente snippet nella console di DevTools per vedere cosa riesci a trovare:
const script = document.createElement('script');
script.src = 'https://unpkg.com/web-vitals@4/dist/web-vitals.attribution.iife.js';
script.onload = function () {
webVitals.onINP(console.log, {reportAllChanges: true});
};
document.head.appendChild(script);