Misurare l'interazione con Next Paint (INP)

1. Introduzione

Questo è un codelab interattivo per imparare a misurare Interaction to Next Paint (INP) utilizzando la libreria web-vitals.

Prerequisiti

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.

Che cosa ti serve

  • Un computer con la possibilità di clonare il codice da GitHub ed eseguire comandi npm.
  • Un editor di testo.
  • Una versione recente di Chrome per il corretto funzionamento di tutte le misurazioni delle interazioni.

2. Configurazione

Ottieni ed esegui il codice

Il codice si trova nel repository web-vitals-codelabs.

  1. Clona il repository nel terminale: git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git.
  2. Passa alla directory clonata: cd web-vitals-codelabs/measuring-inp.
  3. Installa le dipendenze: npm ci.
  4. Avvia il server web: npm run start.
  5. Visita la pagina http://localhost:8080/ nel tuo browser.

Prova la pagina

Questo codelab utilizza Gastropodicon (un noto sito di riferimento sull'anatomia delle lumache) per esplorare i potenziali problemi di INP.

Uno screenshot della pagina demo di Gastropodicon

Prova a interagire con la pagina per avere un'idea di quali interazioni sono lente.

3. Orientarsi in Chrome DevTools

Apri DevTools da Altri strumenti > Menu Strumenti per sviluppatori, facendo clic con il tasto destro del mouse sulla pagina e selezionando Ispeziona oppure utilizzando una scorciatoia da tastiera.

In questo codelab, utilizzeremo sia il riquadro Prestazioni 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 display mobile.
  • Se stai eseguendo il test su un computer desktop o laptop, è probabile che il rendimento sia notevolmente migliore rispetto a quello di un dispositivo mobile reale. Per una visione più realistica delle prestazioni, premi l'icona a forma di ingranaggio in alto a destra nel riquadro Prestazioni, quindi seleziona Raffreddamento della CPU 4x.

Uno screenshot del riquadro Prestazioni di DevTools insieme all'app, con un rallentamento della CPU 4x selezionato

4. Installazione... vital web

web-vitals è una libreria JavaScript per misurare le metriche Web Vitals dell'utente. Puoi utilizzare la libreria per acquisire questi valori, e quindi trasmetterli a un endpoint di analisi per analisi successive, al fine di stabilire quando e dove si verificano interazioni lente.

Esistono diversi modi per aggiungere la libreria a una pagina. Il modo in cui installerai la libreria sul tuo sito dipende da come gestisci le dipendenze, dal processo di compilazione e da altri fattori. Assicurati di controllare la documentazione della libreria per conoscere tutte le opzioni disponibili.

Questo codelab verrà installato da npm e caricherà direttamente lo script per evitare di addentrarsi in un particolare processo di build.

Esistono due versioni di web-vitals che puoi utilizzare:

  • Lo "standard" Se vuoi monitorare i valori delle metriche di Core Web Vitals durante un caricamento pagina, devi utilizzare la build.
  • L'"attribuzione" build aggiunge ulteriori informazioni di debug a ogni metrica per diagnosticare il motivo per cui una metrica ha il valore che corrisponde.

Per misurare l'INP in questo codelab, occorre creare l'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 in fondo a 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. Per impostazione predefinita, web-vitals non lo segnala finché l'utente non abbandona o chiude la pagina. Questo è il comportamento ideale per il beaconing per qualcosa come l'analisi, ma è meno ideale per il debug interattivo.

web-vitals offre l'opzione reportAllChanges per generare report più dettagliati. Quando questa impostazione è attiva, non vengono riportate tutte le interazioni, ma tutte le volte che si verifica un'interazione più lenta di una delle precedenti.

Prova ad aggiungere l'opzione allo script e a interagire nuovamente con la pagina:

<script type="module">
  import {onINP} from './node_modules/web-vitals/dist/web-vitals.attribution.js';

  onINP(console.log, {reportAllChanges: true});
</script>

Se aggiorni la pagina, le interazioni dovrebbero ora essere riportate nella console, aggiornandole ogni volta che c'è una nuova pagina più lenta. Ad esempio, prova a digitare nella casella di ricerca e quindi a eliminare l'input.

Uno screenshot della console DevTools con i messaggi INP stampati correttamente.

5. Che cosa comprende 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 presentano script che richiedono cookie attivati in modo sincrono quando i cookie vengono accettati da un utente, causando un'interazione lenta del clic. Ecco cosa succede qui.

Fai clic su per accettare i cookie (demo) e dai un'occhiata ai dati INP ora registrati nella console DevTools.

Oggetto dati INP registrato nella console DevTools

Queste informazioni di primo livello sono disponibili sia nelle build delle vitali web di attribuzione standard che in quelle di attribuzione:

{
  name: 'INP',
  value: 344,
  rating: 'needs-improvement',
  entries: [...],
  id: 'v4-1715732159298-8028729544485',
  navigationType: 'reload',
  attribution: {...},
}

Il periodo di tempo che intercorre tra il momento in cui l'utente ha fatto clic per passare alla visualizzazione successiva è stato di 344 millisecondi, un "richiede miglioramenti" INP. L'array entries ha tutti i valori PerformanceEntry associati a questa interazione, in questo caso un solo evento di clic.

Per scoprire che cosa accade in questo periodo, tuttavia, siamo più interessati alla proprietà attribution. Per creare i dati di attribuzione, web-vitals individua i frame dell'animazione lunga (LoAF) che si sovrappongono all'evento clic. Il LoAF può quindi fornire dati dettagliati su come il tempo è stato trascorso durante quel frame, dagli script eseguiti al tempo trascorso in un callback, uno stile e un layout di requestAnimationFrame.

Espandi la proprietà attribution per visualizzare ulteriori informazioni. I dati sono molto più ricchi.

attribution: {
  interactionTargetElement: Element,
  interactionTarget: '#confirm',
  interactionType: 'pointer',

  inputDelay: 27,
  processingDuration: 295.6,
  presentationDelay: 21.4,

  processedEventEntries: [...],
  longAnimationFrameEntries: [...],
}

Innanzitutto, ci sono informazioni su con cosa è stata interagito:

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

I tempi sono quindi scommessi in modo generale:

  • inputDelay: il tempo che intercorre tra il momento in cui l'utente ha avviato l'interazione (ad esempio, ha fatto clic con il mouse) e l'inizio dell'esecuzione del listener di eventi per l'interazione in questione. In questo caso, il ritardo di input è stato di soli 27 millisecondi, anche con la limitazione della CPU attivata.
  • processingDuration: il tempo necessario per l'esecuzione fino al completamento dei listener di eventi. Spesso, le pagine hanno più listener per un singolo evento (ad esempio, pointerdown, pointerup e click). Se vengono eseguite tutte nello stesso frame di animazione, verranno unite in questo periodo. In questo caso, la durata di elaborazione richiede 295,6 millisecondi, ovvero la maggior parte del tempo INP.
  • presentationDelay: tempo che intercorre tra il completamento dei listener di eventi e il momento in cui il browser ha completato il disegno del frame successivo. In questo caso, 21,4 millisecondi.

Queste fasi INP possono essere un segnale vitale per diagnosticare ciò che deve essere ottimizzato. La guida di Optimize INP contiene maggiori informazioni su questo argomento.

Più a fondo, processedEventEntries contiene cinque eventi, a differenza del 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 include tutti gli eventi elaborati durante lo stesso frame. Tieni presente che include altri eventi, come mouseover e mousedown, non solo l'evento di clic. Conoscere questi altri eventi può essere fondamentale se anche questi erano lenti, dal momento che hanno contribuito a una velocità di risposta lenta.

Infine c'è l'array longAnimationFrameEntries. Potrebbe 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: [{...}]
}],

Qui ci sono una serie di valori utili, come la suddivisione della quantità di tempo di stile. L'articolo sull'API Long Animation Frames approfondisce queste proprietà. Al momento siamo principalmente interessati alla proprietà scripts, che contiene voci che forniscono dettagli sugli script responsabili del frame a lunga esecuzione:

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 affermare che il tempo è stato speso principalmente in un singolo event-listener, richiamato il giorno BUTTON#confirm.onclick. Possiamo anche vedere l'URL dell'origine dello script e la posizione del carattere in cui è stata definita la funzione.

Conclusione

Che cosa puoi determinare in merito a questa richiesta in base a questi dati di attribuzione?

  • L'interazione è stata attivata da un clic sull'elemento button#confirm (da attribution.interactionTarget e dalla proprietà invoker su una voce di attribuzione script).
  • Il tempo è stato dedicato principalmente all'esecuzione di listener di eventi (da attribution.processingDuration rispetto alla metrica totale value).
  • Il codice listener di eventi lento viene avviato da un listener di clic definito in third-party/cmp.js (da scripts.sourceURL).

Questi dati sono sufficienti per capire dove dobbiamo ottimizzare.

6. Più listener di eventi

Aggiorna la pagina in modo che la console DevTools sia chiara e l'interazione relativa al consenso all'uso dei cookie non sia più l'interazione più lunga.

Inizia a digitare nella casella di ricerca. Che cosa mostrano i dati sull'attribuzione? Cosa credi che stia succedendo?

Dati di attribuzione

Innanzitutto, un'analisi generale 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: [...],
  }
}

Il valore INP è scarso (con la limitazione della CPU attivata) per un'interazione tramite tastiera con l'elemento input#search-terms. La maggior parte del tempo, 1061 millisecondi su un INP totale di 1072 millisecondi, è stata dedicata alla durata dell'elaborazione.

Le voci scripts, tuttavia, sono più interessanti.

Thrashing del layout

La prima voce dell'array scripts ci fornisce un contesto prezioso:

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 listener input (l'invoker è INPUT#search-terms.oninput). Viene assegnato il nome della funzione (handleSearch), così come la posizione del carattere all'interno del file di origine index.js.

Tuttavia, è disponibile una nuova proprietà: forcedStyleAndLayoutDuration. Questo è stato il tempo trascorso all'interno di questa chiamata dello script in cui il browser è stato costretto a modificare il layout della pagina. In altre parole, il 78% del tempo (388 millisecondi su 497) dedicato all'esecuzione di questo listener di eventi è stato effettivamente speso nel thrashing del layout.

Deve essere una priorità assoluta da risolvere.

Listener ripetuti

Individualmente, non c'è niente di particolarmente significativo nelle prossime due voci dello script:

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 listener keyup, in esecuzione una subito dopo l'altra. I listener sono funzioni anonime (quindi non è riportato nulla nella proprietà sourceFunctionName), ma abbiamo comunque un file sorgente e la posizione del carattere, in modo da poter trovare il codice.

Strano è che entrambi provengono dallo stesso file di origine e dalla stessa posizione dei caratteri.

Il browser ha finito per elaborare più pressioni di tasti in un singolo frame di animazione, il che ha portato a questo listener di eventi in esecuzione due volte prima di poter tracciare qualsiasi elemento.

Inoltre, questo effetto può aggravarsi: maggiore è il tempo necessario ai listener di eventi per completare, maggiore è il numero di eventi di input aggiuntivi che possono verificarsi, estendendo molto più a lungo l'interazione lenta.

Poiché si tratta di un'interazione di ricerca/completamento automatico, il debouncing dell'input sarebbe una buona strategia in modo che venga elaborato al massimo una pressione di un tasto per frame.

7. Ritardo input

La causa tipica dei ritardi nell'input (il tempo che intercorre tra l'interazione dell'utente e il momento in cui un listener di eventi può iniziare a elaborare l'interazione) è che il thread principale è occupato. Ciò potrebbe avere diverse cause:

  • La pagina è in fase di caricamento e il thread principale è impegnato nelle attività iniziali di configurazione del DOM, impaginazione e stile della pagina e valutazione ed esecuzione degli script.
  • In genere la pagina è molto impegnata, ad esempio calcoli in esecuzione, animazioni basate su script o annunci.
  • L'elaborazione delle interazioni precedenti richiede così tanto tempo da ritardare quelle future, come illustrato nell'ultimo esempio.

La pagina demo ha una funzione segreta in cui, se fai clic sul logo a forma di lumaca nella parte superiore della pagina, inizierà l'animazione e il lavoro JavaScript del thread principale.

  • Fai clic sul logo della lumaca per avviare l'animazione.
  • Le attività JavaScript vengono attivate quando la lumaca si trova in fondo al rimbalzo. Cerca di interagire con la pagina il più vicino possibile alla parte inferiore del rimbalzo e verifica l'altezza di un INP che riesci ad attivare.

Ad esempio, anche se non attivi altri listener di eventi, ad esempio facendo clic e attivando la casella di ricerca proprio quando la lumaca rimbalza, il lavoro del thread principale farà sì che la pagina non risponda per un certo periodo di tempo.

Su molte pagine, l'intensa attività del thread principale non sarà così ben eseguita, ma questa è una buona dimostrazione per capire come possa essere identificabile nei dati di attribuzione INP.

Di seguito è riportato un esempio di attribuzione, in cui viene selezionato solo l'elemento attivo della casella di ricerca durante il rimbalzo a forma di 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, i listener di eventi sono stati eseguiti rapidamente, mostrando una durata di elaborazione di 4,9 millisecondi e la maggior parte dell'interazione scadente è stata spesa in ritardo di input, prendendo 702,3 millisecondi su un totale di 728.

Può essere difficile eseguire il debug di questa situazione. Anche se sappiamo con che cosa ha interagito l'utente e in che modo, sappiamo anche che quella parte dell'interazione si è conclusa rapidamente e non è stato un problema. Ma è stato un altro aspetto della pagina a ritardare l'elaborazione dell'interazione, ma come facciamo a sapere da dove iniziare a cercare?

Ecco le voci dello script LoAF:

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, rallentava il frame dell'animazione e di conseguenza è inclusa nei dati LoAF uniti all'evento di interazione.

Da qui è possibile vedere come la funzione che ritarda l'elaborazione dell'interazione è stata attivata (da un listener animationiteration), esattamente quale funzione era responsabile e dove si trovava nei file di origine.

8. Ritardo della presentazione: quando un aggiornamento non viene visualizzato

Il ritardo della presentazione misura il tempo che intercorre tra il termine dell'esecuzione dei listener di eventi e il momento in cui il browser non riesce a visualizzare un nuovo frame sullo schermo, mostrando all'utente un feedback visibile.

Aggiorna la pagina per reimpostare nuovamente il valore INP, quindi apri il menu a tre linee. C'è un qualche problema all'apertura.

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 nella presentazione che costituisce la maggior parte dell'interazione lenta. Ciò significa che qualsiasi cosa blocca il thread principale si verifica al termine dei listener 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,
}]

Osservando la singola voce nell'array scripts, si nota il tempo trascorso in un user-callback da un FrameRequestCallback. Questa volta il ritardo della presentazione è causato da un callback di requestAnimationFrame.

9. Conclusione

Aggregazione dei dati dei campi

Vale la pena ricordare che tutto questo è più facile se si esamina una singola voce di attribuzione INP da un singolo caricamento pagina. Come è possibile aggregare questi dati per eseguire il debug di INP in base ai dati dei campi? La quantità di dettagli utili in realtà rende tutto più difficile.

Ad esempio, è estremamente utile sapere quale elemento di pagina è una fonte comune di interazioni lente. Tuttavia, se la pagina ha compilato nomi delle classi CSS che cambiano da build a build, i selettori web-vitals dello stesso elemento potrebbero essere diversi da una build all'altra.

Devi invece pensare alla tua applicazione specifica per determinare ciò che è più utile e come possono essere aggregati i dati. Ad esempio, prima di visualizzare nuovamente i dati di attribuzione, puoi sostituire il selettore web-vitals con un identificatore tuo, in base al componente in cui si trova il target, oppure ai ruoli ARIA soddisfatti dal target.

Allo stesso modo, le voci scripts potrebbero avere hash basati su file nei percorsi sourceURL che le rendono difficili da combinare, ma potresti rimuovere gli hash in base al processo di compilazione noto prima di beaconizzare i dati.

Sfortunatamente, non esiste un percorso facile con dati così complessi, ma anche utilizzarne un sottoinsieme è più utile dell'assenza di dati di attribuzione per il processo di debug.

Attribuzione ovunque!

L'attribuzione INP basata su LoAF è un potente aiuto al debug. Offre dati granulari su ciò che è accaduto specificamente durante un INP. In molti casi, può indicarti la posizione esatta in uno script in cui dovresti iniziare i tuoi sforzi di ottimizzazione.

Ora puoi utilizzare i dati di attribuzione INP su qualsiasi sito.

Anche se non disponi dell'accesso per modificare una pagina, puoi ricreare la procedura da questo codelab eseguendo il seguente snippet nella console 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);

Scopri di più