De Performance API meet het reactievermogen van je live web toepassing op echte gebruikers toestellen en netwerkverbindingen. Het kan helpen knelpunten in je client-side en server-side code op te sporen met:

  • user timing: Aangepaste meting van de prestaties van client-side JavaScript functies
  • paint timing: Meting browser-rendering
  • resource timing: Laadprestaties van assets en Ajax-oproepen
  • navigatie timing: Metrics over het laden van pagina’s, waaronder omleidingen, DNS look-ups, DOM gereedheid, en meer

De API pakt verschillende problemen aan die gepaard gaan met typische prestatie-evaluatie:

  1. Ontwikkelaars testen applicaties vaak op high-end PC’s die met een snel netwerk verbonden zijn. DevTools kan tragere toestellen emuleren, maar het zal niet altijd echte problemen aan het licht brengen wanneer de meeste cliënten een twee jaar oude mobiel gebruiken die verbonden is met luchthaven WiFi.
  2. Opties van derden zoals Google Analytics worden vaak geblokkeerd, wat leidt tot scheve resultaten en veronderstellingen. Ook kun je in sommige landen met privacy-implicaties te maken krijgen.
  3. De Performance API kan verschillende metrieken nauwkeuriger peilen dan methoden als Date().


De volgende paragrafen beschrijven manieren waarop je de Performance API kunt gebruiken. Enige kennis van JavaScript en metrics van het laden van pagina’s is aanbevolen.

Beschikbaarheid van de Performance API

De meeste moderne browsers ondersteunen de Performance API – ook IE10 en IE11 (zelfs IE9 heeft beperkte ondersteuning). Je kunt de aanwezigheid van de API opsporen met:

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

Het is niet mogelijk de API volledig te Polyfillen, dus wees op je hoede voor ontbrekende browsers. Als 90% van je gebruikers tevreden surfen met Internet Explorer 8, zou je slechts 10% van de cliënten meten met meer bekwame toepassingen.

De API kan gebruikt worden in Web Workers, die een manier bieden om complexe berekeningen in een achtergrond thread uit te voeren zonder de browseroperaties te onderbreken.

De meeste API methoden kunnen in server-side Node.js gebruikt worden met de standaard perf_hooks module:

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

console.log( performance.now() );

Deno biedt de standaard Performance API:

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

Je moet scripts uitvoeren met de --allow-hrtime toestemming om hoge-resolutie tijdmeting mogelijk te maken:

deno run --allow-hrtime index.js

Prestaties aan de serverkant zijn meestal gemakkelijker te beoordelen en te beheren omdat ze afhankelijk zijn van belasting, CPU’s, RAM, harde schijven, en limieten van cloud diensten. Hardware upgrades of procesmanagement opties zoals PM2, clustering, en Kubernetes kunnen effectiever zijn dan het refactoren van code.

De volgende secties concentreren zich om deze reden op prestaties aan de client-kant.

Aangepaste prestatiemeting

De Performance API kan gebruikt worden om de uitvoeringssnelheid van je toepassingsfuncties te timen. Je hebt misschien wel eens timing functies gebruikt of tegengekomen met Date():

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

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

De Performance API biedt twee primaire voordelen:

  1. Betere nauwkeurigheid: Date() meet tot op de milliseconde nauwkeurig, maar de Performance API kan fracties van een milliseconde meten (afhankelijk van de browser).
  2. Betere betrouwbaarheid: De gebruiker of het OS kan de systeemtijd veranderen, zodat op Date()-gebaseerde meetgegevens niet altijd nauwkeurig zullen zijn. Dit betekent dat je functies bijzonder traag kunnen lijken als de klokken vooruit gaan!

Het Date() equivalent is performance.now() dat een high-resolution timestamp oplevert die op nul wordt gezet als het proces dat verantwoordelijk is voor het maken van het document begint (de pagina is geladen):

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

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

Een niet-standaard performance.timeOrigin property kan ook een timestamp van 1 januari 1970 teruggeven, hoewel dit niet beschikbaar is in IE en Deno.

performance.now() wordt onpraktisch als je meer dan een paar metingen doet. De Performance API biedt een buffer waarin je events kunt recorden voor latere analyse door een labelnaam door te geven aan 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');

Een array van alle mark objects in de Performance buffer kan worden geëxtraheerd met:

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

Voorbeeldresultaat:

[

  {
    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
  },
...
]

De performance.measure() methode berekent de tijd tussen twee marks en slaat die ook op in de Performance buffer. Je geeft een nieuwe measurenaam door, de de naam van de starting mark (of null om vanaf de paginalading te meten), en de naam van de ending mark (of null om naar de huidige tijd te meten):

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

Een PerformanceMeasure object wordt aan de buffer toegevoegd met de berekende tijdsduur. Om deze waarde te krijgen kun je ofwel een array van alle measures opvragen:

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

of een measure opvragen met zijn naam:

performance.getEntriesByName('init');

Voorbeeldresultaat:

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

Gebruik van de prestatiebuffer

Behalve marks en measures wordt de prestatiebuffer ook gebruikt om automatisch navigatietiming, resourcetiming, en painttiming te recorden (die we later bespreken). Je kunt een array krijgen van alle entries in de buffer:

performance.getEntries();

Standaard bieden de meeste browsers een buffer die tot 150 resource metrics opslaat. Dit zou voldoende moeten zijn voor de meeste beoordelingen, maar je kunt de bufferlimiet verhogen of verlagen als dat nodig is:

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

Merktekens kunnen op naam gewist worden of je kunt een lege waarde opgeven om alle merktekens te wissen:

performance.clearMarks('start:init');

Op dezelfde manier kunnen maatregelen met naam gewist worden of met een lege waarde om alles te wissen:

performance.clearMeasures();

Performancebuffer updates monitoren

Een PerformanceObserver kan veranderingen in de Performance buffer monitoren en een functie uitvoeren als zich bepaalde events voordoen. De syntaxis zal bekend zijn als je ooit gebruikt hebt MutationObserver om te reageren op DOM updates of IntersectionObserver om te detecteren wanneer elementen in de viewport worden geschoven.

Je dient een observer functie definiëren met twee parameters:

  1. een array van observer entries die gedetecteerd zijn, en
  2. het observer object. Indien nodig kan de disconnect() methode kan worden gecallt om de observer te stoppen.
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 }`);
  });

}

De functie wordt doorgegeven aan een nieuw PerformanceObserver object. Zijn observe() methode wordt een array van Performance buffer entryTypes en wordt doorgegeven om te observeren:

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

In dit voorbeeld wordt door het toevoegen van een nieuwe mark of measurement de functie performanceCallback() uitgevoerd. Hoewel het hier alleen berichten logt, zou het gebruikt kunnen worden om een upload van gegevens te starten of verdere berekeningen uit te voeren.

Meten van paint performance

De Paint Timing API is alleen beschikbaar in client-side JavaScript en registreert automatisch twee metrics die belangrijk zijn voor Core Web Vitals:

  1. first-paint: De browser is begonnen met het “schilderen” van de pagina.
  2. first-contentful-paint: De browser heeft het eerste belangrijke item van de DOM inhoud geschilderd, zoals een kop of een afbeelding.

Deze kunnen uit de Performance buffer naar een array geëxtraheerd worden:

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

Wees op je hoede als je dit uitvoert voordat de pagina volledig geladen is; de waarden zijn dan nog niet klaar. Wacht ofwel op de window.load event of gebruik een PerformanceObserver om paint entryTypes te monitoren.

Voorbeeldresultaat:

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

Een trage first-paint wordt vaak veroorzaakt door render-blocking CSS of JavaScript. Het gat naar de first-contentful-paint kan groot zijn als de browser een grote afbeelding moet downloaden of complexe elementen moet renderen.

Prestatiemeting van de resources

Netwerktijden voor resources zoals afbeeldingen, stylesheets, en JavaScript bestanden worden automatisch in de Performance buffer genoteerd. Hoewel je weinig kunt doen om problemen met netwerksnelheid op te lossen (behalve de bestandsgrootte verkleinen), kan het helpen om problemen met grotere resources, trage Ajax reacties, of slecht presterende externe scripts aan het licht te brengen.

Een array van PerformanceResourceTiming metrics kan uit de buffer gehaald worden met:

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

Als alternatief kun je metrics voor een asset ophalen door zijn volledige URL door te geven:

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

Voorbeeldresultaat:

[
  {
    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
  }
]

De volgende propertieskunnen onderzocht worden:

  • name: URL van de resource
  • entryType: “resource”
  • initiatorType: Hoe de resource werd gestart, zoals “script” of “link”
  • serverTiming: Een array van PerformanceServerTiming objecten die door de server in de HTTP Server-Timing header worden doorgegeven (je server-side toepassing zou metrics naar de client kunnen sturen voor verdere analyse).
  • startTime: Timestamp waarop het ophalen begon
  • nextHopProtocol: Gebruikt netwerkprotocol
  • workerStart: Timestamp vóór het starten van een Progressive Web App Service Worker (0 als het verzoek niet onderschept wordt door een Service Worker)
  • redirectStart: Timestamp wanneer een redirect begon
  • redirectEnd: Timestamp na de laatste byte van het laatste redirect antwoord
  • fetchStart: Timestamp vóór het ophalen van de resource
  • domainLookupStart: Timestamp voor een DNS lookup
  • domainLookupEnd: Timestamp na de DNS lookup
  • connectStart: Timestamp vóór het tot stand brengen van een serververbinding
  • connectEnd: Timestamp na het tot stand brengen van een serververbinding
  • secureConnectionStart: Timestamp vóór de SSL handshake
  • requestStart: Timestamp voordat de browser de resource aanvraagt
  • responseStart: Timestamp wanneer de browser de eerste byte gegevens ontvangt
  • responseEnd: Timestamp na ontvangst van de laatste byte of het sluiten van de verbinding
  • duration: Het verschil tussen startTime en responseEnd
  • transferSize: De resourcegrootte in bytes, inclusief de header en de gecomprimeerde body
  • encodedBodySize: De body van de resource in bytes vóór het uitcomprimeren
  • decodedBodySize: Het bronlichaam in bytes na het uitcomprimeren

Dit voorbeeldscript haalt alle Ajax verzoeken op die door de Fetch API gestart zijn en geeft de totale overdrachtsgrootte en -duur:

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 }
);

Navigatie prestatiemeting

Netwerktijden voor het uitladen van de vorige pagina en het laden van de huidige pagina worden automatisch als een enkel object in de Performance buffer opgenomen, het  PerformanceNavigationTiming object.

Extract ze tot een array met behulp van:

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

…of door de URL van de pagina door te geven aan .getEntriesByName():

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

De metric is identiek aan die voor resources, maar bevat ook pagina-specifieke waarden:

  • entryType: Bv. “navigation”
  • type: Ofwel “navigate”, “reload”, “back_forward”, of “prerender”
  • redirectCount: Het aantal redirects
  • unloadEventStart: Timestamp vóór de unload event van het vorige document
  • unloadEventEnd: Timestamp na de unload event van het vorige document
  • domInteractive: Timestamp wanneer de browser de HTML heeft geparst en het DOM heeft geconstrueerd
  • domContentLoadedEventStart: Timestamp voordat de DOMContentLoaded gebeurtenis van het document afgaat
  • domContentLoadedEventEnd: Timestamp nadat document’s DOMContentLoaded gebeurtenis afgaat
  • domComplete: Timestamp nadat DOM constructie en DOMContentLoaded events zijn voltooid
  • loadEventStart: Timestamp voordat het laadgebeurtenis van de pagina is afgegaan
  • loadEventEnd: Timestamp nadat het laadevent van de pagina is afgegaan en alle assets beschikbaar zijn

Typische zaken zijn:

  • Een lange vertraging tussen unloadEventEnd en domInteractive. Dit kan wijzen op een trage serverrespons.
  • Een lange vertraging tussen domContentLoadedEventStart en domComplete. Dit kan erop wijzen dat de opstartscripts van de pagina te traag zijn.
  • Een lange vertraging tussen domComplete en loadEventEnd. Dit kan erop wijzen dat de pagina te veel assets heeft of dat het te lang duurt voor er verschillende geladen zijn.

Recording en analyse van prestaties

Met de Performance API kun je real-world gebruiksgegevens verzamelen en die naar een server uploaden voor verdere analyse. Je zou een externe dienst zoals Google Analytics kunnen gebruiken om de gegevens op te slaan, maar het risico bestaat dat het externe script geblokkeerd wordt of nieuwe prestatieproblemen introduceert. Je eigen oplossing kan aan je eisen aangepast worden om ervoor te zorgen dat monitoring geen invloed heeft op andere functionaliteit.

Wees op je hoede voor situaties waarin statistieken niet bepaald kunnen worden – misschien omdat gebruikers op oude browsers zitten, JavaScript blokkeren, of achter een bedrijfs-proxy zitten. Begrijpen welke gegevens ontbreken kan beter werken dan veronderstellingen maken op basis van onvolledige informatie.

Idealiter zullen je analysescripts de prestaties niet nadelig beïnvloeden door ingewikkelde berekeningen uit te voeren of grote hoeveelheden gegevens te uploaden. Overweeg het gebruik van webworkers en het minimaliseren van het gebruik van synchrone localStorage calls. Het is altijd mogelijk om ruwe gegevens later in batch te verwerken.

Wees tenslotte op je hoede voor uitschieters zoals zeer snelle of zeer trage apparaten en verbindingen die de statistieken nadelig beïnvloeden. Bijvoorbeeld, als negen gebruikers een pagina in twee seconden laden maar de tiende een download van 60 seconden ervaart, komt de gemiddelde vertraging uit op bijna 8 seconden. Een meer realistische maatstaf is de mediaan (2 seconden) of het 90ste percentiel (9 op de 10 gebruikers ervaren een laadtijd van 2 seconden of minder).

Samenvatting

Webprestaties blijven een belangrijke factor voor ontwikkelaars. Gebruikers verwachten dat sites en toepassingen op de meeste apparaten responsief zijn. Ook zoekmachineoptimalisatie kan eronder lijden, want tragere sites worden in Google afgestraft.

Er zijn veel prestatie-monitoring tools, maar de meeste beoordelen uitvoeringssnelheden aan de kant van de server of gebruiken een beperkt aantal goede clients om de browserrendering te beoordelen. De Performance API biedt een manier om échte gebruikerscijfers te verzamelen die op geen enkele andere manier te berekenen zouden zijn.

Craig Buckler

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