The JavaScript Performance API
The Performance API gives you precise timing data, custom performance markers, and real-time observation of browser metrics. No libraries needed.
High-Resolution Timing with performance.now()
Date.now() gives you millisecond precision, but the browser can do better. performance.now() returns a DOMHighResTimeStamp with microsecond resolution, and it’s monotonic—it never goes backwards.
const start = performance.now();
// Some operation
const result = Array.from({ length: 10000 }, (_, i) => i * 2)
.filter(n => n % 3 === 0)
.reduce((a, b) => a + b, 0);
const end = performance.now();
console.log(`Took ${(end - start).toFixed(2)}ms`);
// Output: Took <varies>ms
The difference between two performance.now() calls gives you wall-clock time that accounts for throttling, background tabs, and garbage collection pauses. Useful for benchmarking without external tools.
performance.timeOrigin returns the absolute timestamp when the performance context was created—typically page load. You can combine it with performance.now() to correlate relative timings with wall-clock time:
const absoluteTime = performance.timeOrigin + performance.now();
console.log(absoluteTime); // Unix timestamp in milliseconds
Custom Marks and Measures
performance.mark() creates a named timestamp you can reference later. performance.measure() calculates the duration between two marks or from a mark to now.
performance.mark("loop-start");
// Your code here
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += i;
}
performance.mark("loop-end");
performance.measure("loop-duration", "loop-start", "loop-end");
const measurement = performance.getEntriesByName("loop-duration")[0];
console.log(measurement.duration);
// Output: <varies>ms
Clean up marks when done to avoid memory buildup:
performance.mark("cleanup-example");
// ... code ...
performance.clearMark("cleanup-example");
Without cleanup, performance.getEntries() accumulates entries over time. For long-running applications, clear marks you no longer need.
Inspecting Performance Entries
The browser automatically records performance entries for navigation, resource loads, and paint operations. performance.getEntries() returns all of them at once.
const entries = performance.getEntries();
entries.forEach(entry => {
// Paint entries have no duration; use startTime for those
if (entry.duration !== undefined) {
console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`);
} else {
console.log(`${entry.name}: ${entry.startTime.toFixed(2)}ms (no duration)`);
}
});
Output (example):
https://example.com/: <varies>ms
<script> <varies>ms
<link> styles.css <varies>ms
First Paint: <varies>ms (no duration)
Filter by type when you only need specific entries:
const navigationEntries = performance.getEntriesByType("navigation");
const paintEntries = performance.getEntriesByType("paint");
const resourceEntries = performance.getEntriesByType("resource");
Each entry type has different properties. Navigation entries include DNS lookup time, connection negotiation, and document load times. Paint entries have startTime but no duration—use startTime for paint timing.
Real-Time Observation with PerformanceObserver
Polling getEntries() is inefficient. PerformanceObserver lets you subscribe to new entries as they happen—perfect for detecting slow resources or monitoring long tasks.
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration !== undefined) {
console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`);
} else {
console.log(`${entry.name}: ${entry.startTime.toFixed(2)}ms`);
}
}
});
observer.observe({ entryTypes: ["measure", "paint"] });
The callback fires whenever entries matching your filter are added. Useful for tracking metrics over time without manual polling.
Disconnect the observer when done:
observer.disconnect();
For one-time checks with buffered: true, disconnect inside the callback to avoid missing asynchronously delivered entries:
const paintObserver = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (entry.duration !== undefined) {
console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`);
} else {
console.log(`${entry.name}: ${entry.startTime.toFixed(2)}ms`);
}
});
paintObserver.disconnect();
});
paintObserver.observe({ type: "paint", buffered: true });
The buffered: true option includes existing entries from before the observer started. Without it, you only see new entries going forward.
Long Tasks with the Long Tasks API
Tasks that block the main thread for more than 50ms are classified as Long Tasks. The Long Tasks API lets you observe them:
const longTaskObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`Long task: ${entry.duration.toFixed(2)}ms`);
}
});
longTaskObserver.observe({ entryTypes: ["longtask"] });
Long Tasks are a key input to Interaction to Next Paint (INP), a Core Web Vital that measures responsiveness.
Event Timing with the Event Timing API
The Event Timing API exposes the latency of browser events, including the Total Blocking Time (TBT) metric and INP contributions:
const eventObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.processingStart !== undefined) {
const inputDelay = entry.processingStart - entry.startTime;
const processingTime = entry.processingEnd - entry.processingStart;
console.log(`${entry.name}: input delay ${inputDelay.toFixed(2)}ms, processing ${processingTime.toFixed(2)}ms`);
}
}
});
eventObserver.observe({ entryTypes: ["event"] });
Navigation Timing
The Navigation Timing API extends performance with detailed timing breakdown for page loads. Access it via performance.getEntriesByType("navigation")[0].
const [nav] = performance.getEntriesByType("navigation");
console.log(`DNS lookup: ${nav.domainLookupEnd - nav.domainLookupStart}ms`);
console.log(`TCP handshake: ${nav.connectEnd - nav.connectStart}ms`);
console.log(`DOM interactive: ${nav.domInteractive}ms`);
console.log(`DOM complete: ${nav.domComplete}ms`);
console.log(`Page load: ${nav.loadEventEnd - nav.startTime}ms`);
Output (example):
DNS lookup: <varies>ms
TCP handshake: <varies>ms
DOM interactive: <varies>ms
DOM complete: <varies>ms
Page load: <varies>ms
This tells you exactly where time goes during page load—useful for diagnosing bottlenecks without opening DevTools.
Resource Timing for Fetched Resources
Resource timing tracks individual assets: scripts, stylesheets, images, and XHR requests.
const resourceObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 100) {
console.warn(`Slow resource: ${entry.name} took ${entry.duration.toFixed(0)}ms`);
}
}
});
resourceObserver.observe({ entryTypes: ["resource"] });
Track specific resource types by inspecting the initiatorType:
resourceObserver.observe({
entryTypes: ["resource"],
buffered: true
});
// Trigger some resources
fetch("/api/data").then(r => r.json());
const img = new Image();
img.src = "/image.png";
In production, use this to flag resources that take longer than acceptable thresholds and identify where optimization efforts will have the most impact.
The performance Global
Everything lives on the performance global object. Check for API availability before using it:
if (typeof performance !== "undefined" && performance.now) {
const start = performance.now();
// Use the API
}
In Node.js, perf_hooks.performance provides similar functionality. The browser and server APIs share the same mental model but differ in available entry types.
Common Pitfalls
Marks and measures won’t appear in production builds if the browser’s performance observer is disabled or throttled. Always test timing-sensitive code in a real environment, not just development.
Buffer sizes are finite. If you accumulate entries without reading them, older entries get dropped. Process entries regularly or use observers to drain the buffer.
Garbage collection pauses affect timing. A fast operation followed by a GC sweep can appear slow. Run multiple iterations and look at distributions, not single measurements.
performance.now() is relative to page load, not an absolute timestamp. For absolute times, use Date.now() or combine performance.timeOrigin with performance.now(). For measuring durations, performance.now() is the right tool.
See Also
- console.time() and console.timeEnd() — browser console timing helpers
- Promise timing — measuring async operation latency
- structuredClone() — performance characteristics of the structured clone algorithm
- MDN: Performance API
- Navigation Timing API
- Resource Timing API