Präzise Laufzeitmessungen mit performance.now() und PerformanceObserver
Date.now() liefert Millisekunden mit einer Präzision, die für ernsthafte Performance-Messungen nicht ausreicht, und tappt in die Falle der Systemuhr-Synchronisation. Die JavaScript Performance API liefert Sub-Millisekunden-Zeitstempel, strukturierte Marks und Measures, einen nicht blockierenden Observer-Mechanismus – und direkten Zugang zu Web Vitals ohne externe Bibliothek.
Inhaltsverzeichnis
- 1. Warum Date.now() für Performance-Messungen ungeeignet ist
- 2. performance.now(): Sub-Millisekunden-Präzision und Monotonie
- 3. User Timing: performance.mark() und performance.measure()
- 4. PerformanceObserver: asynchrone und nicht blockierende Messungen
- 5. Resource Timing: Netzwerk-Ladezeiten analysieren
- 6. Long Tasks API: Jank und Blocking-Zeit erkennen
- 7. Web Vitals direkt mit der Performance API messen
- 8. Performance API in Node.js: perf_hooks und Worker Threads
- 9. Performance API: Methoden im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Date.now() für Performance-Messungen ungeeignet ist
Der häufigste Ansatz für Laufzeitmessungen in JavaScript ist die Differenz zweier Date.now()-Aufrufe. Das Problem: Date.now() gibt die Anzahl der Millisekunden seit dem Unix-Epoch zurück und ist an die Systemuhr gebunden. Die Systemuhr kann sich ändern – durch NTP-Synchronisation, manuelle Anpassung oder Sommerzeit-Umstellung. Das bedeutet, dass ein Zeitintervall, das mit Date.now() gemessen wird, negativ sein kann, wenn die Systemuhr während der Messung zurückgestellt wird. Für kurze Zeitspannen liefert Date.now() außerdem nur ganzzahlige Millisekunden ohne Sub-Millisekunden-Auflösung.
Die JavaScript Performance API löst beide Probleme. performance.now() basiert auf einer monotonen Uhr, die garantiert niemals rückwärts läuft, unabhängig von Systemuhr-Änderungen. Die zurückgegebene Zahl ist ein Fließkomma-Wert mit Sub-Millisekunden-Präzision – in Browsern aus Sicherheitsgründen auf 0,1 ms gerundet, in Nicht-Sicherheitskontexten (z. B. Node.js) mit voller CPU-Auflösung. Der Startzeitpunkt ist der Navigationsstart der Seite, nicht das Unix-Epoch, was die Werte kleiner und lesbarer macht.
2. performance.now(): Sub-Millisekunden-Präzision und Monotonie
performance.now() gibt die Anzahl der Millisekunden seit dem Navigationsstart der aktuellen Seite zurück, als Fließkomma-Zahl mit bis zu fünf Dezimalstellen. Der Wert ist monoton: Er nimmt niemals ab, auch nicht bei Systemuhr-Änderungen. Das macht ihn zur zuverlässigen Basis für alle Performance API-Messungen. Der Unterschied zwischen zwei performance.now()-Aufrufen ist immer nicht-negativ und spiegelt die tatsächlich verstrichene Rechen- und Wartezeit wider.
Ein häufiger Missverständnis: performance.now() misst nicht CPU-Zeit, sondern Wanduhrenzeit – einschließlich Wartezeiten auf I/O, Netzwerk und Timer. Wer nur CPU-Zeit messen will, braucht Worker Threads oder server-seitige Messungen. Im Browser ist Wanduhrenzeit in der Regel das, was man messen will: die Zeit, die ein Nutzer tatsächlich auf eine Reaktion wartet. Die Performance API liefert genau diese Zahl, präzise und ohne Systemuhr-Abhängigkeit.
// performance.now() — monotonic, sub-millisecond, navigation-relative
const start = performance.now();
// Simulate some work
for (let i = 0; i < 1_000_000; i++) { Math.sqrt(i); }
const end = performance.now();
const delta = end - start; // always >= 0, sub-millisecond precision
console.log(`Loop took ${delta.toFixed(3)} ms`);
// Compare with Date.now() — integer ms, wall-clock dependent
const t1 = Date.now();
for (let i = 0; i < 1_000_000; i++) { Math.sqrt(i); }
const t2 = Date.now();
console.log(`Date.now delta: ${t2 - t1} ms`); // integer, could be 0 for fast code
// Safe for cross-frame timing — origin-relative, not epoch-relative
// performance.timeOrigin gives the absolute epoch offset
console.log(`Page loaded at: ${new Date(performance.timeOrigin).toISOString()}`);
console.log(`Current offset: ${performance.now().toFixed(3)} ms after load`);
3. User Timing: performance.mark() und performance.measure()
Die User Timing API ist die ausdrucksstärkste Schicht der Performance API für Anwendungscode. Mit performance.mark("name") wird ein benannter Zeitstempel im Performance-Buffer gespeichert. Mit performance.measure("name", "start-mark", "end-mark") wird die Differenz zwischen zwei Marks als PerformanceMeasure-Eintrag gespeichert und steht automatisch in Browser-DevTools sichtbar unter dem Timeline-Abschnitt "Timings". Das bedeutet, dass eigene Messungen direkt neben Browser-internen Timings wie Navigation und Resource Loading visualisiert werden.
Ein wesentlicher Vorteil gegenüber einem manuellen performance.now()-Differenz-Ansatz: Marks und Measures werden in einem separaten Buffer gespeichert und können jederzeit mit performance.getEntriesByType("mark") und performance.getEntriesByType("measure") abgerufen werden – auch nach dem gemessenen Code-Abschnitt, etwa in einem Error-Handler oder einem Berichterstattungs-Callback. Mit performance.clearMarks() und performance.clearMeasures() können veraltete Einträge aus dem Buffer entfernt werden, um Speicherlecks bei lang laufenden Anwendungen zu vermeiden.
// User Timing API — named marks and measures visible in DevTools Timeline
async function loadUserData(userId) {
performance.mark("loadUserData:start");
// Phase 1: fetch user
performance.mark("fetchUser:start");
const user = await fetch(`/api/users/${userId}`).then(r => r.json());
performance.mark("fetchUser:end");
performance.measure("Fetch User", "fetchUser:start", "fetchUser:end");
// Phase 2: fetch user's orders
performance.mark("fetchOrders:start");
const orders = await fetch(`/api/orders?userId=${userId}`).then(r => r.json());
performance.mark("fetchOrders:end");
performance.measure("Fetch Orders", "fetchOrders:start", "fetchOrders:end");
performance.mark("loadUserData:end");
performance.measure("Total loadUserData", "loadUserData:start", "loadUserData:end");
// Retrieve measures for reporting
const measures = performance.getEntriesByType("measure");
measures.forEach(m => console.log(`${m.name}: ${m.duration.toFixed(2)} ms`));
// Clean up to avoid buffer bloat in long-running apps
performance.clearMarks();
performance.clearMeasures();
return { user, orders };
}
4. PerformanceObserver: asynchrone und nicht blockierende Messungen
Der PerformanceObserver ist die moderne, reaktive Schnittstelle zur Performance API. Statt einen Buffer abzufragen (getEntriesByType()), registriert man einen Callback, der asynchron aufgerufen wird, sobald neue Performance-Einträge eines bestimmten Typs verfügbar sind. Das ist besonders wichtig für Metriken, die kontinuierlich anfallen – Resource Timing, Long Tasks, Layout Shifts – wo ein polling-basierter Ansatz entweder zu spät käme oder unnötig CPU-Zeit verbrauchen würde.
Der PerformanceObserver blockiert den Haupt-Thread nicht: Der Callback wird als Mikrotask nach dem aktuellen Event-Loop-Tick ausgeführt. Das macht ihn ideal für Produktions-Monitoring, das die Nutzererfahrung nicht beeinträchtigen darf. Ein einzelner Observer kann mehrere Eintragstypen gleichzeitig beobachten (observe({ type: "mark" }), observe({ type: "measure" })). Mit observer.disconnect() wird der Observer beendet, wenn keine weiteren Einträge erwartet werden.
5. Resource Timing: Netzwerk-Ladezeiten analysieren
Die Resource Timing API ist Teil der Performance API und erfasst automatisch detaillierte Timing-Informationen für alle Ressourcen, die eine Seite lädt: Scripts, Stylesheets, Bilder, Fonts und XHR/Fetch-Anfragen. Für jeden Eintrag stehen Zeitstempel zur Verfügung: startTime, fetchStart, domainLookupStart, connectStart, requestStart, responseStart, responseEnd. Aus diesen Timestamps lassen sich DNS-Lookup-Zeit, TCP-Verbindungszeit, TTFB (Time to First Byte) und Download-Zeit berechnen.
Besonders wertvoll ist die Resource Timing API für die Identifikation von Netzwerk-Bottlenecks ohne externe Monitoring-Tools. Wenn ein Script 800 ms lädt, zeigt die Resource Timing API, ob das Problem in der DNS-Auflösung, der TCP-Verbindung, dem TTFB oder dem eigentlichen Download liegt. Das ermöglicht gezielte Optimierungen: CDN-Setup, Preconnect-Hints, HTTP/2-Multiplexing oder Caching-Strategien – datengetrieben, auf Basis echter Nutzerdaten, nicht Laborwerte.
// PerformanceObserver for Resource Timing — non-blocking, reactive
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Only analyze resources slower than 200 ms
if (entry.duration < 200) continue;
const dns = entry.domainLookupEnd - entry.domainLookupStart;
const tcp = entry.connectEnd - entry.connectStart;
const ttfb = entry.responseStart - entry.requestStart;
const download = entry.responseEnd - entry.responseStart;
console.group(`Slow resource: ${entry.name}`);
console.log(`Total: ${entry.duration.toFixed(0)} ms`);
console.log(`DNS: ${dns.toFixed(0)} ms`);
console.log(`TCP: ${tcp.toFixed(0)} ms`);
console.log(`TTFB: ${ttfb.toFixed(0)} ms`);
console.log(`Download: ${download.toFixed(0)} ms`);
console.groupEnd();
}
});
// buffered: true catches entries that happened before observer was created
observer.observe({ type: "resource", buffered: true });
// Long Tasks API: detect main-thread blocking > 50 ms
const longTaskObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn(`Long Task detected: ${entry.duration.toFixed(0)} ms`);
// entry.attribution gives the script/frame responsible
}
});
longTaskObserver.observe({ type: "longtask", buffered: true });
6. Long Tasks API: Jank und Blocking-Zeit erkennen
Ein „Long Task" ist definiert als jede JavaScript-Ausführung, die den Haupt-Thread länger als 50 ms blockiert. Lange Tasks verhindern, dass der Browser auf Benutzereingaben reagiert, Animationen flüssig läuft und der Scroll-Interaktion reibungslos ist – das Phänomen wird als "Jank" bezeichnet. Die Long Tasks API innerhalb der Performance API ermöglicht es, solche blockierenden Codeabschnitte automatisch zu erkennen und zu protokollieren, ohne dass die Entwickler jeden Codeabschnitt manuell instrumentieren müssen.
Jeder Long-Task-Eintrag enthält einen attribution-Array, der Informationen über den Container (Dokument, iframe, Worker) liefert, in dem der Long Task ausgeführt wurde. In Kombination mit User Timing Marks kann man genaue Aussagen treffen: "Dieser Long Task von 120 ms wurde während unserer loadUserData-Measure ausgeführt." Das ist das Fundament für evidenzbasiertes Performance-Optimieren statt Raten nach DevTools-Screenshots.
7. Web Vitals direkt mit der Performance API messen
Web Vitals – LCP (Largest Contentful Paint), FID (First Input Delay), CLS (Cumulative Layout Shift) – sind die von Google definierten Core-Metriken für die Nutzererfahrung im Web. Alle drei sind direkt über die Performance API messbar, ohne die offizielle web-vitals-Bibliothek einbinden zu müssen. LCP wird über den PerformanceObserver mit type: "largest-contentful-paint" erfasst. CLS mit type: "layout-shift". FID mit type: "first-input". Das ermöglicht Real-User-Monitoring (RUM) direkt im eigenen JavaScript-Code.
Ein vollständiges RUM-Setup mit der Performance API sendet diese Metriken asynchron an ein Analyse-Backend – über navigator.sendBeacon(), das auch beim Seitenverlassen garantiert abgesendet wird. Das ist der Kern von Google Search Console, Lighthouse und Chrome UX Report: Echte Nutzerdaten, gemessen im echten Browser, über die nativ eingebaute Performance API. Wer die web-vitals-Bibliothek einsetzt, kann das Bundle-Gewicht durch eine eigene, schlanke Implementierung auf Basis der Performance API deutlich reduzieren.
| API | Zweck | Eintrag-Typ | Schlüssel-Eigenschaft |
|---|---|---|---|
| performance.now() | Monotoner Zeitstempel | — | Sub-ms, nie negativ |
| User Timing | Code-Abschnitte benennen | mark, measure | DevTools-Integration, Buffer-Abfrage |
| Resource Timing | Netzwerk-Ladezeiten | resource | DNS, TCP, TTFB, Download |
| Long Tasks API | Jank erkennen | longtask | duration > 50 ms, attribution |
| Layout Instability | CLS messen | layout-shift | value, hadRecentInput |
8. Performance API in Node.js: perf_hooks und Worker Threads
In Node.js ist die Performance API über das eingebaute Modul node:perf_hooks verfügbar. performance.now() und PerformanceObserver funktionieren dort mit derselben Semantik wie im Browser, aber mit höherer Präzision – ohne die Sicherheitsbegrenzung auf 0,1 ms, die Browser aus Timing-Attack-Schutz einführen. Das macht Node.js-seitige Messungen mit der Performance API besonders präzise für Mikro-Benchmarks und Algorithmus-Vergleiche.
In Worker Threads steht die Performance API vollständig zur Verfügung. Jeder Worker hat seinen eigenen Zeitursprung, relativ zum Start des Workers. Das ermöglicht unabhängige Laufzeitmessungen in jedem Thread, ohne dass sich Messungen aus dem Haupt-Thread und Worker-Threads gegenseitig beeinflussen. Für Benchmarks in Node.js empfiehlt sich außerdem performance.timerify(fn), das eine Funktion in eine gemessene Variante transformiert und automatisch für jeden Aufruf einen function-Eintrag im Performance-Buffer erstellt.
// Node.js Performance API via perf_hooks (same API as browsers)
import { performance, PerformanceObserver } from "node:perf_hooks";
// Wrap function to auto-instrument with User Timing
function withTiming(name, fn) {
return async function (...args) {
performance.mark(`${name}:start`);
try {
return await fn.apply(this, args);
} finally {
performance.mark(`${name}:end`);
performance.measure(name, `${name}:start`, `${name}:end`);
}
};
}
// Observer reports all measures to console
const obs = new PerformanceObserver((items) => {
items.getEntries().forEach(entry => {
console.log(`[perf] ${entry.name}: ${entry.duration.toFixed(3)} ms`);
});
obs.disconnect();
});
obs.observe({ type: "measure", buffered: true });
// timerify: auto-instrumented function (Node.js specific)
import { createHistogram } from "node:perf_hooks";
const histogram = createHistogram();
const timerified = performance.timerify(
function computeHash(data) { /* … */ },
{ histogram }
);
// After multiple calls: histogram.mean, histogram.percentile(99), etc.
Mironsoft
Web Performance Optimierung und Real-User-Monitoring
Web Vitals und Ladezeiten systematisch verbessern?
Wir implementieren Real-User-Monitoring mit der nativen Performance API, identifizieren Long Tasks und Netzwerk-Bottlenecks und liefern konkrete Optimierungsmaßnahmen für LCP, CLS und FID.
RUM-Setup
Natives Performance API RUM ohne externe Bibliothek – Web Vitals, Long Tasks, Resource Timing
Performance-Audit
Analyse von LCP, CLS, TTFB und Long Tasks auf Basis echter Nutzerdaten
Optimierung
Konkrete Maßnahmen: Preconnect, Code-Splitting, CDN, Caching und Bundle-Optimierung
10. Zusammenfassung
Die JavaScript Performance API ist das vollständigste native Werkzeug für Laufzeitmessungen im Browser und in Node.js. performance.now() liefert monotone Sub-Millisekunden-Zeitstempel ohne Systemuhr-Abhängigkeit. User Timing mit performance.mark() und performance.measure() macht eigene Code-Abschnitte sichtbar – direkt in den DevTools. Der PerformanceObserver ermöglicht reaktive, nicht blockierende Erfassung von Resource Timing, Long Tasks und Web Vitals. Die Long Tasks API identifiziert Jank-Quellen ohne manuelles Instrumentieren.
Wer diese Werkzeuge kombiniert, baut ein vollständiges Real-User-Monitoring-System ohne externe Bibliothek, das echte Nutzerdaten für LCP, CLS und FID liefert. Das ist die Grundlage für evidenzbasiertes Performance-Optimieren – nicht auf Basis von Laborwerten aus Lighthouse, sondern auf Basis der Performance API-Daten echter Nutzer in echten Netzwerkbedingungen.
JavaScript Performance API — Das Wichtigste auf einen Blick
performance.now()
Monoton, Sub-Millisekunden, navigationRelativ. Nie negativ, keine Systemuhr-Abhängigkeit. Basis aller Performance-Messungen.
User Timing
performance.mark() und performance.measure() machen Code-Abschnitte in DevTools sichtbar und im Buffer abrufbar.
PerformanceObserver
Reaktiv, nicht blockierend. Beobachtet Resource Timing, Long Tasks, Web Vitals asynchron – ideal für RUM in der Produktion.
Long Tasks API
Erkennt Jank: JS-Ausführung > 50 ms. Attribution-Array zeigt, welches Script blockiert. Keine manuelle Instrumentierung nötig.