Die Performance API misst die Reaktionsfähigkeit deiner Live-Webanwendung auf realen Nutzergeräten und Netzwerkverbindungen. Sie kann dabei helfen, Engpässe in deinem client- und serverseitigen Code zu identifizieren:

  • user timing: Benutzerdefinierte Messung der Performance clientseitiger JavaScript-Funktionen
  • paint timing: Browser-Rendering-Metriken
  • ressource timing: Ladeleistung von Assets und Ajax-Aufrufen
  • navigation timing: Metriken zum Laden von Seiten, einschließlich Weiterleitungen, DNS-Lookups, DOM-Bereitschaft und mehr

Die API löst mehrere Probleme, die mit der typischen Performancebewertung verbunden sind:

  1. Entwickler testen ihre Anwendungen oft auf High-End-PCs, die mit einem schnellen Netzwerk verbunden sind. DevTools kann zwar langsamere Geräte emulieren, aber es zeigt nicht immer Probleme auf, wenn die meisten Kunden ein zwei Jahre altes Handy benutzen, das mit dem Flughafen-WiFi verbunden ist.
  2. Optionen von Drittanbietern wie Google Analytics sind oft blockiert, was zu verzerrten Ergebnissen und Annahmen führt. Außerdem kann es in einigen Ländern zu Datenschutzproblemen kommen.
  3. Die Performance API kann verschiedene Metriken besser messen als Methoden wie Date().


Die folgenden Abschnitte beschreiben, wie du die Performance API nutzen kannst. Einige Kenntnisse über JavaScript und Seitenlademetriken werden empfohlen.

Verfügbarkeit der Performance API

Die meisten modernen Browser unterstützen die Performance API – auch IE10 und IE11 (sogar IE9 hat eine begrenzte Unterstützung). Du kannst das Vorhandensein der API feststellen, indem du:

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

Es ist nicht möglich, die API vollständig mit Polyfill zu füllen, also sei vorsichtig bei fehlenden Browsern. Wenn 90 % deiner Nutzer/innen mit dem Internet Explorer 8 surfen, würdest du nur 10 % der Kunden mit leistungsfähigeren Anwendungen messen.

Die API kann in Web Workern verwendet werden, die komplexe Berechnungen in einem Hintergrund-Thread ausführen, ohne den Browserbetrieb zu unterbrechen.

Die meisten API-Methoden können in serverseitigem Node.js mit dem Standardmodul perf_hooks verwendet werden:

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

console.log( performance.now() );

Deno bietet die Standard-Performance-API:

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

Um eine hochauflösende Zeitmessung zu ermöglichen, musst du Skripte mit der Berechtigung --allow-hrtime ausführen:

deno run --allow-hrtime index.js

Die serverseitige Performance ist in der Regel einfacher zu beurteilen und zu verwalten, da sie von der Last, den CPUs, dem RAM, den Festplatten und den Grenzen der Cloud-Dienste abhängt. Hardware-Upgrades oder Prozessmanagement-Optionen wie PM2, Clustering und Kubernetes können effektiver sein als das Refactoring von Code.

Aus diesem Grund konzentrieren sich die folgenden Abschnitte auf die clientseitige Performance.

Benutzerdefinierte Performancemessung

Die Performance API kann verwendet werden, um die Ausführungsgeschwindigkeit deiner Anwendungsfunktionen zu messen. Vielleicht hast du schon einmal Timing-Funktionen verwendet oder bist ihnen begegnet, indem du Date() benutzt hast:

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

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

Die Performance-API bietet zwei Hauptvorteile:

  1. Bessere Genauigkeit: Date() misst auf die nächste Millisekunde genau, aber die Performance API kann Bruchteile von Millisekunden messen (abhängig vom Browser).
  2. Bessere Zuverlässigkeit: Der Benutzer oder das Betriebssystem können die Systemzeit ändern, so dass die auf Date() basierenden Metriken nicht immer genau sind. Das bedeutet, dass deine Funktionen besonders langsam erscheinen können, wenn die Uhren vorgestellt werden!

Die Date() Entsprechung ist performance.now() die einen hochauflösenden Zeitstempel zurückgibt, der auf Null gesetzt wird, wenn der Prozess, der für die Erstellung des Dokuments verantwortlich ist, beginnt (die Seite wurde geladen):

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

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

Eine nicht standardisierte performance.timeOrigin Eigenschaft kann auch einen Zeitstempel vom 1. Januar 1970 zurückgeben, obwohl dies im IE und in Deno nicht verfügbar ist.

performance.now() wird unpraktisch, wenn du mehr als ein paar Messungen machst. Die Performance-API bietet einen Puffer, in dem du Ereignisse für eine spätere Analyse aufzeichnen kannst, indem du einen Labelnamen an performance.mark() übergibst:

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

Ein Array mit allen Markierungsobjekten im Performance-Puffer kann mit extrahiert werden:

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

Beispielergebnis:

[

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

Die performance.measure() Methode berechnet die Zeit zwischen zwei Marken und speichert sie ebenfalls im Performance-Puffer. Du übergibst einen neuen Taktnamen, den Namen der Startmarke (oder null, um ab dem Laden der Seite zu messen) und den Namen der Endmarke (oder null, um die aktuelle Zeit zu messen):

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

Ein PerformanceMeasure-Objekt wird an den Puffer mit der berechneten Zeitdauer angehängt. Um diesen Wert zu erhalten, kannst du entweder ein Array mit allen Messungen anfordern:

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

oder eine Maßnahme anhand ihres Namens abfragen:

performance.getEntriesByName('init');

Beispielergebnis:

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

Verwendung des Performance-Puffers

Der Performance-Puffer wird nicht nur für Markierungen und Messungen verwendet, sondern auch für die automatische Aufzeichnung der Navigations-, Ressourcen- und Paint-Zeiten (die wir später besprechen werden). Du kannst ein Array mit allen Einträgen im Puffer erhalten:

performance.getEntries();

Standardmäßig bieten die meisten Browser einen Puffer, der bis zu 150 Ressourcenmetriken speichert. Das sollte für die meisten Bewertungen ausreichen, aber du kannst die Puffergrenze bei Bedarf erhöhen oder verringern:

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

Markierungen können nach Namen gelöscht werden oder du kannst einen leeren Wert angeben, um alle Markierungen zu löschen:

performance.clearMarks('start:init');

Ebenso können Maßnahmen nach Namen oder mit einem leeren Wert gelöscht werden, um alle zu löschen:

performance.clearMeasures();

Überwachung von Performance-Puffer-Updates

Ein PerformanceObserver kann Änderungen im Performance-Puffer überwachen und eine Funktion ausführen, wenn bestimmte Ereignisse eintreten. Die Syntax wird dir vertraut sein, wenn du schon einmal mit MutationObserver gearbeitet hast, um auf DOM-Aktualisierungen zu reagieren oder mit IntersectionObserver um zu erkennen, wenn Elemente in den Viewport gescrollt werden.

Du musst eine Beobachterfunktion mit zwei Parametern definieren:

  1. ein Array von Beobachtereinträgen, die erkannt wurden, und
  2. das Beobachterobjekt. Falls nötig, kann ihre disconnect() Methode aufgerufen werden, um den Beobachter zu 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 }`);
  });

}

Die Funktion wird an ein neues PerformanceObserver-Objekt übergeben. Seiner observe() Methode wird ein Array von Performance-Buffer entryTypes übergeben, die beobachtet werden sollen:

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

In diesem Beispiel wird beim Hinzufügen einer neuen Marke oder Maßnahme die Funktion performanceCallback() ausgeführt. Sie protokolliert hier zwar nur Meldungen, könnte aber auch dazu verwendet werden, einen Daten-Upload auszulösen oder weitere Berechnungen durchzuführen.

Messung der Paint-Performance

Die Paint Timing API ist nur in clientseitigem JavaScript verfügbar und zeichnet automatisch zwei Kennzahlen auf, die für Core Web Vitals wichtig sind:

  1. first-paint: Der Browser hat begonnen, die Seite zu zeichnen.
  2. first-contentful-paint: Der Browser hat das erste signifikante Element des DOM-Inhalts gezeichnet, z. B. eine Überschrift oder ein Bild.

Diese können aus dem Performance-Puffer in ein Array extrahiert werden:

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

Sei vorsichtig, wenn du diesen Vorgang ausführst, bevor die Seite vollständig geladen ist; die Werte sind dann noch nicht fertig. Entweder warte auf das window.load Ereignis oder verwende einen PerformanceObserver um paint entryTypes zu überwachen.

Beispielergebnis:

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

Ein langsames First-Paint  wird oft durch Rendering-blockierende CSS oder JavaScript verursacht. Die Lücke zum First-Contentful-Paint kann groß sein, wenn der Browser ein großes Bild herunterladen oder komplexe Elemente rendern muss.

Messung der Ressourcen-Performance

Die Netzwerkzeiten für Ressourcen wie Bilder, Stylesheets und JavaScript-Dateien werden automatisch im Performance-Puffer aufgezeichnet. Du kannst zwar wenig tun, um Probleme mit der Netzwerkgeschwindigkeit zu lösen (außer die Dateigröße zu reduzieren), aber es kann helfen, Probleme mit größeren Assets, langsamen Ajax-Antworten oder schlecht funktionierenden Skripten von Drittanbietern aufzuzeigen.

Ein Array von PerformanceResourceTiming-Metriken kann aus dem Puffer extrahiert werden:

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

Alternativ kannst du Metriken für ein Asset abrufen, indem du seine vollständige URL angibst:

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

Beispielergebnis:

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

Die folgenden Eigenschaften können untersucht werden:

  • name: URL der Ressource
  • entryType: „Ressource“
  • initiatorType: Wie die Ressource initiiert wurde, z. B. „Skript“ oder „Link“
  • serverTiming: Ein Array von PerformanceServerTiming Objekten, die vom Server im HTTP Server-Timing-Header übergeben werden (deine serverseitige Anwendung könnte Metriken zur weiteren Analyse an den Client senden)
  • startTime: Zeitstempel, wann der Abruf begann
  • nextHopProtocol: Verwendetes Netzwerkprotokoll
  • workerStart: Zeitstempel vor dem Start eines Progressive Web App Service Workers (0, wenn die Anfrage nicht von einem Service Worker abgefangen wird)
  • redirectStart: Zeitstempel, wann eine Weiterleitung gestartet wurde
  • redirectEnd: Zeitstempel nach dem letzten Byte der letzten Redirect-Antwort
  • fetchStart: Zeitstempel vor dem Abruf der Ressource
  • domainLookupStart: Zeitstempel vor einem DNS-Lookup
  • domainLookupEnd: Zeitstempel nach dem DNS-Lookup
  • connectStart: Zeitstempel vor dem Aufbau einer Serververbindung
  • connectEnd: Zeitstempel nach dem Aufbau einer Serververbindung
  • secureConnectionStart: Zeitstempel vor dem SSL-Handshake
  • requestStart: Zeitstempel, bevor der Browser die Ressource anfordert
  • responseStart: Zeitstempel, wenn der Browser das erste Byte der Daten erhält
  • responseEnd: Zeitstempel nach dem Empfang des letzten Bytes oder dem Schließen der Verbindung
  • duration: Die Differenz zwischen startTime und responseEnd
  • transferSize: Die Größe der Ressource in Bytes einschließlich Header und komprimiertem Body
  • encodedBodySize: Der Body der Ressource in Bytes vor der Dekomprimierung
  • decodedBodySize: Der Body der Ressource in Bytes nach der Dekomprimierung

Dieses Beispielskript ruft alle Ajax-Anfragen ab, die von der Fetch-API initiiert wurden, und gibt die Gesamtgröße und Dauer der Übertragung zurück:

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

Messung der Navigations-Performance

Die Netzwerkzeiten für das Entladen der vorherigen Seite und das Laden der aktuellen Seite werden automatisch im Performance-Puffer als ein einziges PerformanceNavigationTiming objekt.

Extrahiere sie mit in ein Array:

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

…oder indem du die URL der Seite an .getEntriesByName() übergibst:

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

Die Metriken sind identisch mit denen für Ressourcen, enthalten aber auch seitenbezogene Werte:

  • entryType: Z.B. „Navigation“
  • type: Entweder „navigate“, „reload“, „back_forward“ oder „prerender“
  • redirectCount: Die Anzahl der Weiterleitungen
  • unloadEventStart: Zeitstempel vor dem unload-Ereignis des vorherigen Dokuments
  • unloadEventEnd: Zeitstempel nach dem unload-Ereignis des vorherigen Dokuments
  • domInteractive: Zeitstempel, wenn der Browser das HTML geparst und das DOM aufgebaut hat
  • domContentLoadedEventStart: Zeitstempel, bevor das Ereignis DOMContentLoaded des Dokuments ausgelöst wird
  • domContentLoadedEventEnd: Zeitstempel, nachdem das DOMContentLoaded-Ereignis des Dokuments ausgelöst wurde
  • domComplete: Zeitstempel nach Abschluss der DOM-Konstruktion und der DOMContentLoaded-Ereignisse
  • loadEventStart: Zeitstempel, bevor das Ereignis „Seite laden“ ausgelöst wurde
  • loadEventEnd: Zeitstempel, nachdem das Ereignis zum Laden der Seite ausgelöst wurde und alle Assets verfügbar sind

Typische Ausgaben sind:

  • Eine lange Verzögerung zwischen unloadEventEnd und domInteractive. Dies könnte ein Hinweis auf eine langsame Serverantwort sein.
  • Eine lange Verzögerung zwischen domContentLoadedEventStart und domComplete. Dies könnte darauf hinweisen, dass die Skripte zum Starten der Seite zu langsam sind.
  • Eine lange Verzögerung zwischen domComplete und loadEventEnd. Dies könnte darauf hindeuten, dass die Seite zu viele Assets hat oder einige zu lange zum Laden brauchen.

Performance-Aufzeichnung und -Analyse

Die Performance-API ermöglicht es dir, reale Nutzungsdaten zu sammeln und sie zur weiteren Analyse auf einen Server hochzuladen. Du könntest einen Drittanbieterdienst wie Google Analytics nutzen, um die Daten zu speichern, aber es besteht die Gefahr, dass das Skript des Drittanbieters blockiert wird oder neue Leistungsprobleme verursacht. Deine eigene Lösung kann an deine Anforderungen angepasst werden, um sicherzustellen, dass die Überwachung keine Auswirkungen auf andere Funktionen hat.

Sei vorsichtig bei Situationen, in denen keine Statistiken ermittelt werden können – vielleicht weil die Nutzer/innen alte Browser benutzen, JavaScript blockieren oder sich hinter einem Firmen-Proxy befinden. Zu verstehen, welche Daten fehlen, kann fruchtbarer sein, als Annahmen zu treffen, die auf unvollständigen Informationen beruhen.

Im Idealfall beeinträchtigen deine Analyseskripte die Performance nicht, indem sie komplexe Berechnungen durchführen oder große Datenmengen hochladen. Erwäge den Einsatz von Web Workern und minimiere die Verwendung von synchronen localStorage-Aufrufen. Es ist immer möglich, die Rohdaten später im Stapel zu verarbeiten.

Schließlich solltest du dich vor Ausreißern wie sehr schnellen oder sehr langsamen Geräten und Verbindungen hüten, die sich negativ auf die Statistiken auswirken. Wenn zum Beispiel neun Nutzer eine Seite in zwei Sekunden laden, der zehnte aber 60 Sekunden braucht, beträgt die durchschnittliche Latenzzeit fast 8 Sekunden. Eine realistischere Kennzahl ist der Median (2 Sekunden) oder das 90. Perzentil (9 von 10 Nutzern erleben eine Ladezeit von 2 Sekunden oder weniger).

Zusammenfassung

Die Web-Performance bleibt ein wichtiger Faktor für Entwickler/innen. Die Nutzer erwarten, dass Websites und Anwendungen auf den meisten Geräten ansprechend sind. Auch die Suchmaschinenoptimierung kann beeinträchtigt werden, da langsamere Websites bei Google herabgestuft werden.

Es gibt viele Tools zur Performanceüberwachung, aber die meisten bewerten die serverseitigen Ausführungsgeschwindigkeiten oder verwenden eine begrenzte Anzahl von fähigen Clients, um das Browser-Rendering zu beurteilen. Die Performance API bietet eine Möglichkeit, echte Nutzerkennzahlen zu sammeln, die auf andere Weise nicht berechnet werden können.

Craig Buckler

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