La Performance API misura la reattività della vostra applicazione web live su dispositivi e connessioni di rete reali. Può aiutare a identificare i colli di bottiglia nel codice lato client e lato server grazie a:

  • user timing: Misurazione personalizzata delle prestazioni delle funzioni JavaScript lato client
  • paint timing: Metriche di rendering del browser
  • resource timing: Prestazioni di caricamento delle risorse e delle chiamate Ajax
  • navigation timing: Metriche di caricamento della pagina, compresi i reindirizzamenti, le ricerche DNS, la prontezza del DOM e altro ancora

Questo tipo di API risolve diversi problemi associati alla tipica valutazione delle prestazioni:

  1. Gli sviluppatori spesso testano le applicazioni su PC di fascia alta collegati a una rete veloce. DevTools può emulare dispositivi più lenti, ma non sempre mette in evidenza i problemi del mondo reale quando la maggior parte dei clienti usa un cellulare di due anni fa connesso al WiFi dell’aeroporto.
  2. Le opzioni di terze parti come Google Analytics sono spesso bloccate, il che porta a risultati e ipotesi distorte. Inoltre, in alcuni Paesi si possono verificare implicazioni per la privacy.
  3. La Performance API può misurare con precisione diverse metriche e meglio di metodi come Date().


Le sezioni seguenti descrivono i modi in cui potete usare la Performance API. È necessario avere una certa conoscenza di JavaScript e delle metriche di caricamento delle pagine.

Disponibilità della Performance API

La maggior parte dei browser moderni supporta la Performance API, compresi IE10 e IE11 (anche IE9 ha un supporto limitato). Potete rilevare la presenza dell’API in questo modo:

if ('performance' in window) {
  // use Performance API
}

Non è possibile effettuare un Polyfill completo dell’API, quindi fate attenzione ai browser mancanti. Se il 90% dei vostri utenti naviga felicemente con Internet Explorer 8, misurerete solo il 10% dei clienti con applicazioni più performanti.

L’API può essere utilizzata nei Web Worker, che forniscono un modo per eseguire calcoli complessi in un thread in background senza interrompere le operazioni del browser.

La maggior parte dei metodi dell’API può essere utilizzata in Node.js lato server con il modulo standard perf_hooks:

// Node.js performance
import { performance } from 'node:perf_hooks';
// or in Common JS: const { performance } = require('node:perf_hooks');

console.log( performance.now() );

Deno fornisce la Performance API standard:

// Deno performance
console.log( performance.now() );

Per abilitare la misurazione del tempo ad alta risoluzione, dovrai eseguire gli script con l’autorizzazione --allow-hrtime:

deno run --allow-hrtime index.js

Le prestazioni lato server sono solitamente più facili da valutare e gestire perché dipendono dal carico, dalla CPU, dalla RAM, dai dischi rigidi e dai limiti dei servizi cloud. Gli aggiornamenti dell’hardware o le opzioni di gestione dei processi come PM2, clustering e Kubernetes possono essere più efficaci della rifattorizzazione del codice.

Per questo motivo, le sezioni seguenti si concentrano sulle prestazioni del lato client.

Misurazione Personalizzata delle Prestazioni

La Performance API può essere usata per cronometrare la velocità di esecuzione delle funzioni della vostra applicazione. Potreste aver usato o incontrato funzioni di cronometraggio che usano Date():

const timeStart = new Date();
runMyCode();
const timeTaken = new Date() - timeStart;

console.log(`runMyCode() executed in ${ timeTaken }ms`);

La Performance API offre due vantaggi principali:

  1. Migliore accuratezza: Date() misura al millisecondo, ma la Performance API può misurare frazioni di millisecondo (a seconda del browser).
  2. Migliore affidabilità: L’utente o il sistema operativo possono modificare l’ora del sistema, quindi le metriche basate su Date() non saranno sempre accurate. Ciò significa che le vostre funzioni potrebbero apparire particolarmente lente quando gli orologi si spostano in avanti!

L’equivalente di Date() è performance.now() che restituisce un timestamp ad alta risoluzione che viene azzerato quando inizia il processo responsabile della creazione del documento (la pagina è stata caricata):

const timeStart = performance.now();
runMyCode();
const timeTaken = performance.now() - timeStart;

console.log(`runMyCode() executed in ${ timeTaken }ms`);

Una proprietà non standard performance.timeOrigin può anche restituire un timestamp a partire dal 1 gennaio 1970, anche se questo non è disponibile in IE e Deno.

performance.now() diventa poco pratico quando si effettuano più di poche misurazioni. La Performance API fornisce un buffer in cui è possibile registrare gli eventi per un’analisi successiva, passando il nome di un’etichetta a performance.mark():

performance.mark('start:app');
performance.mark('start:init');

init(); // run initialization functions

performance.mark('end:init');
performance.mark('start:funcX');

funcX(); // run another function

performance.mark('end:funcX');
performance.mark('end:app');

Un array di tutti gli oggetti marcati nel buffer delle prestazioni può essere estratto utilizzando:

const mark = performance.getEntriesByType('mark');

Risultato di esempio:

[

  {
    detail: null
    duration: 0
    entryType: "mark"
    name: "start:app"
    startTime: 1000
  },
  {
    detail: null
    duration: 0
    entryType: "mark"
    name: "start:init"
    startTime: 1001
  },
  {
    detail: null
    duration: 0
    entryType: "mark"
    name: "end:init"
    startTime: 1100
  },
...
]

Il metodo performance.measure() calcola il tempo tra due segni e lo memorizza nel buffer delle prestazioni. Si passa il nome di una nuova misura, il nome del segno iniziale (o null per misurare dal caricamento della pagina) e il nome del segno finale (o null per misurare l’ora corrente):

performance.measure('init', 'start:init', 'end:init');

Un oggetto PerformanceMeasure viene aggiunto al buffer con la durata calcolata. Per ottenere questo valore, potete richiedere un array di tutte le misure:

const measure = performance.getEntriesByType('measure');

oppure richiedere una misura in base al suo nome:

performance.getEntriesByName('init');

Esempio di risultato:

[
  {
    detail: null
    duration: 99
    entryType: "measure"
    name: "init"
    startTime: 1001
  }
]

Usare il Performance Buffer

Oltre ai segni e alle misure, il Performance Buffer si usa per registrare automaticamente il navigation timing, il resource timing e il paint timing (di cui parleremo più avanti). Potete ottenere un array di tutte le voci del buffer:

performance.getEntries();

Per impostazione predefinita, la maggior parte dei browser fornisce un buffer che memorizza fino a 150 metriche delle risorse. Questo dovrebbe essere sufficiente per la maggior parte delle valutazioni, ma potete aumentare o diminuire il limite del buffer se necessario:

// record 500 metrics
performance.setResourceTimingBufferSize(500);

I punteggi possono essere cancellati per nome oppure potete specificare un valore vuoto per cancellare tutti i punteggi:

performance.clearMarks('start:init');

Allo stesso modo, le misure possono essere cancellate con un nome o con un valore vuoto per cancellarle tutte:

performance.clearMeasures();

Monitoraggio degli Aggiornamenti del Performance Buffer

Un PerformanceObserver può monitorare le modifiche al buffer delle prestazioni ed eseguire una funzione quando si verificano eventi specifici. La sintassi vi sarà familiare se avete usato il metodo MutationObserver per rispondere agli aggiornamenti del DOM o IntersectionObserver per rilevare quando gli elementi vengono fatti scorrere nel viewport.

Dovete definire una funzione osservatore con due parametri:

  1. un array delle entry observer rilevate e
  2. l’observer object. Se necessario, il suo metodo disconnect() può essere richiamato per fermare l’observer.
function performanceCallback(list, observer) {

  list.getEntries().forEach(entry => {
    console.log(`name    : ${ entry.name }`);
    console.log(`type    : ${ entry.type }`);
    console.log(`start   : ${ entry.startTime }`);
    console.log(`duration: ${ entry.duration }`);
  });

}

La funzione viene passata a un nuovo oggetto PerformanceObserver. Il suo metodo observe() riceve un array entryTypes di Performance Buffer da osservare:

let observer = new PerformanceObserver( performanceCallback );
observer.observe({ entryTypes: ['mark', 'measure'] });

In questo esempio, l’aggiunta di un nuovo punteggio o di una nuova misura esegue la funzione performanceCallback(). Anche se in questo caso si limita a registrare i messaggi, potrebbe essere utilizzata per attivare un caricamento di dati o per effettuare ulteriori calcoli.

Misurare la Paint Performance

L’API Paint Timing è disponibile solo in JavaScript lato client e registra automaticamente due metriche importanti per i Core Web Vitals:

  1. first-paint: Il browser ha iniziato a rendere la pagina.
  2. first-contentful-paint: Il browser ha reso il primo elemento significativo del contenuto DOM, come un titolo o un’immagine.

Queste possono essere estratte dal buffer delle prestazioni in un array:

const paintTimes = performance.getEntriesByType('paint');

Fate attenzione a eseguire questa operazione prima che la pagina sia completamente caricata; i valori non saranno pronti. Attendete l’evento window.load o usate un parametro PerformanceObserver per monitorare paint entryTypes.

Esempio di risultato:

[
  {
    "name": "first-paint",
    "entryType": "paint",
    "startTime": 812,
    "duration": 0
  },
  {
    "name": "first-contentful-paint",
    "entryType": "paint",
    "startTime": 856,
    "duration": 0
  }
]

Un first-paint lento è spesso causato da CSS o JavaScript che bloccano il rendering. Lo scarto rispetto al first-contentful-paint potrebbe essere elevato se il browser deve scaricare un’immagine di grandi dimensioni o renderizzare elementi complessi.

Misurazione delle Resource Performance

I tempi di rete di risorse come immagini, fogli di stile e file JavaScript vengono registrati automaticamente nel Performance Buffer. Sebbene non si possa fare molto per risolvere i problemi di velocità della rete (a parte ridurre le dimensioni dei file), questa misurazione può aiutare a evidenziare i problemi legati alle risorse più grandi, alle risposte Ajax lente o agli script di terze parti che funzionano male.

Un array di metriche PerformanceResourceTiming può essere estratto dal buffer usando:

const resources = performance.getEntriesByType('resource');

In alternativa, potete recuperare le metriche di un asset passando il suo URL completo:

const resource = performance.getEntriesByName('https://test.com/script.js');

Esempio di risultato:

[
  {
    connectEnd: 195,
    connectStart: 195,
    decodedBodySize: 0,
    domainLookupEnd: 195,
    domainLookupStart: 195,
    duration: 2,
    encodedBodySize: 0,
    entryType: "resource",
    fetchStart: 195,
    initiatorType: "script",
    name: "https://test.com/script.js",
    nextHopProtocol: "h3",
    redirectEnd: 0,
    redirectStart: 0,
    requestStart: 195,
    responseEnd: 197,
    responseStart: 197,
    secureConnectionStart: 195,
    serverTiming: [],
    startTime: 195,
    transferSize: 0,
    workerStart: 195
  }
]

È possibile esaminare le seguenti proprietà:

  • name: URL della risorsa
  • entryType: “resource”
  • initiatorType: Come è stata avviata la risorsa, per esempio “script” o “link”
  • serverTiming: Un array di oggetti PerformanceServerTiming passati dal server nell’header HTTP Server-Timing (l’applicazione lato server potrebbe inviare le metriche al client per ulteriori analisi)
  • startTime: Timestamp dell’inizio del fetch
  • nextHopProtocol: Protocollo di rete utilizzato
  • workerStart: Timestamp prima dell’avvio di un Service Worker di Progressive Web App (0 se la richiesta non viene intercettata da un Service Worker)
  • redirectStart: Timestamp dell’inizio di un redirect
  • redirectEnd: Timestamp dopo l’ultimo byte dell’ultima risposta di redirect
  • fetchStart: Timestamp prima del recupero della risorsa
  • domainLookupStart: Timestamp prima di una ricerca DNS
  • domainLookupEnd: Timestamp dopo la ricerca DNS
  • connectStart: Timestamp prima di stabilire una connessione al server
  • connectEnd: Timestamp dopo aver stabilito una connessione al server
  • secureConnectionStart: Timestamp prima dell’handshake SSL
  • requestStart: Timestamp prima che il browser richieda la risorsa
  • responseStart: Timestamp in cui il browser riceve il primo byte di dati
  • responseEnd: Timestamp dopo la ricezione dell’ultimo byte o la chiusura della connessione
  • duration: La differenza tra startTime e responseEnd
  • transferSize: La dimensione della risorsa in byte che include l’header e il body compresso
  • encodedBodySize: Il corpo della risorsa in byte prima della decompressione
  • decodedBodySize: Il corpo della risorsa in byte dopo la decompressione

Questo esempio di script recupera tutte le richieste Ajax avviate dall’API Fetch e restituisce la dimensione e la durata totale del trasferimento:

const fetchAll = performance.getEntriesByType('resource')
  .filter( r => r.initiatorType === 'fetch')
  .reduce( (sum, current) => {
    return {
      transferSize: sum.transferSize += current.transferSize,
      duration: sum.duration += current.duration
    }
  },
  { transferSize: 0, duration: 0 }
);

Misurazione della Navigation Performance

I tempi di rete per lo scaricamento della pagina precedente e il caricamento della pagina corrente vengono registrati automaticamente nel Performance Buffer come un singolo oggetto PerformanceNavigationTiming.

Estraetelo in un array utilizzando:

const pageTime = performance.getEntriesByType('navigation');

…o passando l’URL della pagina a .getEntriesByName():

const pageTiming = performance.getEntriesByName(window.location);

Le metriche sono identiche a quelle delle risorse ma includono anche valori specifici della pagina:

  • entryType: Per esempio “navigation”
  • tipo: O “navigation”, “reload”, “back_forward” o “prerender”
  • redirectCount: Il numero di reindirizzamenti
  • unloadEventStart: Timestamp prima dell’evento di scarico del documento precedente
  • unloadEventEnd: Timestamp dopo l’evento di unload del documento precedente
  • domInteractive: Timestamp in cui il browser ha analizzato l’HTML e costruito il DOM
  • domContentLoadedEventStart: Timestamp prima che si attivi l’evento DOMContentLoaded del documento
  • domContentLoadedEventEnd: Timestamp dopo il completamento dell’evento DOMContentLoaded del documento
  • domComplete: Timestamp dopo che la costruzione del DOM e gli eventi DOMContentLoaded sono stati completati
  • loadEventStart: Timestamp prima che l’evento load della pagina sia stato attivato
  • loadEventEnd: Timestamp dopo l’evento di caricamento della pagina e tutte le risorse sono disponibili

I problemi tipici includono:

  • Un lungo ritardo tra unloadEventEnd e domInteractive. Questo potrebbe indicare una risposta lenta del server.
  • Un lungo ritardo tra domContentLoadedEventStart e domComplete. Questo potrebbe indicare che gli script di avvio della pagina sono troppo lenti.
  • Un lungo ritardo tra domComplete e loadEventEnd. Questo potrebbe indicare che la pagina ha troppe risorse o che alcune di esse richiedono troppo tempo per essere caricate.

Registrazione e Analisi della Performance

La Performance API vi permette di raccogliere i dati di utilizzo reali e di caricarli su un server per ulteriori analisi. Potreste usare un servizio di terze parti come Google Analytics per archiviare i dati, ma c’è il rischio che lo script di terze parti venga bloccato o introduca nuovi problemi di prestazioni. La vostra soluzione può essere personalizzata in base alle vostre esigenze per garantire che il monitoraggio non influisca sulle altre funzionalità.

Fate attenzione alle situazioni in cui non è possibile determinare le statistiche, magari perché gli utenti usano browser vecchi, bloccano JavaScript o si trovano dietro un proxy aziendale. Capire quali dati mancano può essere più proficuo che fare ipotesi basate su informazioni incomplete.

Idealmente, i vostri script di analisi non devono avere un impatto negativo sulle prestazioni eseguendo calcoli complessi o caricando grandi quantità di dati. Considerate l’utilizzo di web worker e di ridurre al minimo l’uso di chiamate sincrone a localStorage. È sempre possibile elaborare i dati grezzi in batch in un secondo momento.

Infine, fate attenzione ai valori anomali, come i dispositivi e le connessioni molto veloci o molto lente, che influiscono negativamente sulle statistiche. Per esempio, se 9 utenti caricano una pagina in 2 secondi ma il decimo la scarica in 60 secondi, la latenza media è di quasi 8 secondi. Un parametro più realistico è la mediana (2 secondi) o il 90° percentile (9 utenti su 10 hanno un tempo di caricamento di 2 secondi o meno).

Riepilogo

Le performance web rimangono un fattore critico per chi lavora come frontend developer. Gli utenti si aspettano che i siti e le applicazioni siano responsive sulla maggior parte dei dispositivi. Anche l’ottimizzazione per i motori di ricerca può risentirne perché i siti più lenti vengono declassati da Google.

Esistono molti strumenti di monitoraggio delle prestazioni, ma la maggior parte di essi valuta la velocità di esecuzione lato server o usa un numero limitato di client idonei per valutare il rendering del browser. La Performance API offre un modo per raccogliere le metriche reali degli utenti che non sarebbe possibile calcolare in altro modo.

Craig Buckler

Freelance UK web developer, writer, and speaker. Has been around a long time and rants about standards and performance.