Measuring Performance with the JavaScript Performance API
Use the Performance API to measure load times, mark custom timings, observe long tasks, and profile real-user performance in JavaScript applications.
What you'll learn
- ✓How to use performance.mark() and performance.measure() for custom timings
- ✓Accessing navigation, resource, and paint timing data
- ✓Using PerformanceObserver to monitor performance in real time
Prerequisites
- •Basic JavaScript and DOM knowledge
- •Familiarity with browser developer tools
The Performance API is a collection of browser interfaces that give you precise, high-resolution timing data about your application. Unlike Date.now(), which has millisecond precision and can be affected by clock adjustments, performance.now() provides sub-millisecond timestamps from a monotonic clock that only moves forward.
performance.now()
The simplest entry point. Returns a high-resolution timestamp in milliseconds, relative to the page’s navigation start.
const start = performance.now();
// Do some work
for (let i = 0; i < 1_000_000; i++) {
Math.sqrt(i);
}
const end = performance.now();
console.log(`Computation took ${(end - start).toFixed(2)}ms`);
This is more reliable than Date.now() for benchmarking because the monotonic clock is not affected by system clock changes or NTP adjustments.
Marks and Measures
For structured timing, use performance.mark() to create named timestamps and performance.measure() to compute the duration between them.
// Mark the start of an operation
performance.mark("fetch-start");
const response = await fetch("/api/data");
const data = await response.json();
// Mark the end
performance.mark("fetch-end");
// Measure the duration between marks
const measure = performance.measure("fetch-duration", "fetch-start", "fetch-end");
console.log(`Fetch took ${measure.duration.toFixed(2)}ms`);
Retrieving entries
// Get all marks
const marks = performance.getEntriesByType("mark");
// Get all measures
const measures = performance.getEntriesByType("measure");
// Get entries by name
const fetchMeasures = performance.getEntriesByName("fetch-duration");
// Clear entries to prevent memory growth
performance.clearMarks();
performance.clearMeasures();
Adding metadata to marks
Marks accept a detail property for attaching contextual data.
performance.mark("component-render", {
detail: { component: "ProductList", itemCount: 42 },
});
const entry = performance.getEntriesByName("component-render")[0];
console.log(entry.detail); // { component: "ProductList", itemCount: 42 }
Navigation Timing
The Navigation Timing API provides detailed timing for the page load lifecycle. Access it via performance.getEntriesByType("navigation").
const [nav] = performance.getEntriesByType("navigation");
console.log("DNS lookup:", nav.domainLookupEnd - nav.domainLookupStart, "ms");
console.log("TCP connect:", nav.connectEnd - nav.connectStart, "ms");
console.log("Request:", nav.responseStart - nav.requestStart, "ms");
console.log("Response:", nav.responseEnd - nav.responseStart, "ms");
console.log("DOM interactive:", nav.domInteractive - nav.startTime, "ms");
console.log("DOM complete:", nav.domComplete - nav.startTime, "ms");
console.log("Full load:", nav.loadEventEnd - nav.startTime, "ms");
This data is invaluable for understanding where time is spent during page loads — whether the bottleneck is DNS, server response, or DOM parsing.
Resource Timing
Every resource (script, stylesheet, image, font, fetch request) gets a PerformanceResourceTiming entry.
const resources = performance.getEntriesByType("resource");
resources.forEach((r) => {
console.log(`${r.name}: ${r.duration.toFixed(2)}ms (${r.transferSize} bytes)`);
});
Finding slow resources
function getSlowResources(thresholdMs = 500) {
return performance
.getEntriesByType("resource")
.filter((r) => r.duration > thresholdMs)
.sort((a, b) => b.duration - a.duration)
.map((r) => ({
url: r.name,
duration: Math.round(r.duration),
size: r.transferSize,
type: r.initiatorType,
}));
}
console.table(getSlowResources());
Checking cache hits
const resources = performance.getEntriesByType("resource");
resources.forEach((r) => {
const cached = r.transferSize === 0 && r.decodedBodySize > 0;
if (cached) {
console.log(`Cache hit: ${r.name}`);
}
});
PerformanceObserver
Instead of polling for entries, PerformanceObserver lets you react to performance events as they happen.
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`[${entry.entryType}] ${entry.name}: ${entry.duration.toFixed(2)}ms`);
}
});
// Observe multiple entry types
observer.observe({ entryTypes: ["measure", "resource", "longtask"] });
Observing paint events
const paintObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`${entry.name}: ${entry.startTime.toFixed(2)}ms`);
}
});
paintObserver.observe({ type: "paint", buffered: true });
// Logs: "first-paint: 312.50ms"
// Logs: "first-contentful-paint: 487.30ms"
The buffered: true option retrieves entries that occurred before the observer was created.
Monitoring Long Tasks
Long tasks are JavaScript executions that block the main thread for more than 50ms. They are a primary cause of poor interactivity.
const longTaskObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn(`Long task detected: ${entry.duration.toFixed(2)}ms`, {
startTime: entry.startTime,
attribution: entry.attribution,
});
}
});
longTaskObserver.observe({ type: "longtask", buffered: true });
Building a performance reporter
Here is a practical utility that collects key metrics and sends them to an analytics endpoint.
class PerformanceReporter {
constructor(endpoint) {
this.endpoint = endpoint;
this.metrics = {};
}
collectNavigationTiming() {
const [nav] = performance.getEntriesByType("navigation");
if (!nav) return;
this.metrics.ttfb = nav.responseStart - nav.requestStart;
this.metrics.domInteractive = nav.domInteractive - nav.startTime;
this.metrics.domComplete = nav.domComplete - nav.startTime;
this.metrics.fullLoad = nav.loadEventEnd - nav.startTime;
}
collectPaintTiming() {
const paints = performance.getEntriesByType("paint");
for (const paint of paints) {
if (paint.name === "first-paint") {
this.metrics.firstPaint = paint.startTime;
}
if (paint.name === "first-contentful-paint") {
this.metrics.fcp = paint.startTime;
}
}
}
collectResourceSummary() {
const resources = performance.getEntriesByType("resource");
this.metrics.resourceCount = resources.length;
this.metrics.totalTransferSize = resources.reduce(
(sum, r) => sum + (r.transferSize || 0), 0
);
this.metrics.slowestResource = resources.reduce(
(max, r) => (r.duration > max.duration ? r : max),
{ duration: 0, name: "" }
).name;
}
async report() {
this.collectNavigationTiming();
this.collectPaintTiming();
this.collectResourceSummary();
// Use sendBeacon to ensure data is sent even on page unload
const blob = new Blob([JSON.stringify(this.metrics)], {
type: "application/json",
});
navigator.sendBeacon(this.endpoint, blob);
}
}
// Report after the page fully loads
window.addEventListener("load", () => {
setTimeout(() => {
const reporter = new PerformanceReporter("/api/metrics");
reporter.report();
}, 0);
});
Server Timing
Servers can send timing data via the Server-Timing HTTP header, and the Performance API exposes it.
Server-Timing: db;dur=53, cache;desc="Cache Read";dur=2.4
const [nav] = performance.getEntriesByType("navigation");
nav.serverTiming.forEach((entry) => {
console.log(`${entry.name}: ${entry.duration}ms -- ${entry.description}`);
});
// "db: 53ms -- "
// "cache: 2.4ms -- Cache Read"
This bridges server-side and client-side performance measurement, letting you see the full picture without separate logging systems.
Summary
The Performance API gives you precise, structured access to timing data across the entire page lifecycle:
performance.now()provides high-resolution monotonic timestamps for benchmarking.performance.mark()andperformance.measure()create named timing checkpoints.- Navigation Timing breaks down page load into DNS, TCP, request, response, and DOM phases.
- Resource Timing reveals the cost of every individual asset.
PerformanceObserverreacts to performance events in real time, including long tasks and paint events.- Server Timing bridges server-side metrics into the client-side Performance API.
Use these tools to build data-driven performance optimization rather than guessing where your bottlenecks are.
Related articles
- Go Go Profiling with pprof: CPU, Memory, and Goroutines
Learn how to profile Go applications using pprof to find CPU bottlenecks, memory leaks, and goroutine issues with practical examples.
- Python Python Profiling: Find and Fix Performance Bottlenecks
Learn how to profile Python code with cProfile, line_profiler, memory_profiler, and timeit to identify slow functions, memory leaks, and optimize runtime performance.
- JavaScript JavaScript Event Loop Explained: How Async Code Really Works
Understand how the JavaScript event loop handles async operations including the call stack, microtasks, macrotasks, and execution order with practical examples.
- JavaScript How to Find and Fix JavaScript Memory Leaks
Learn how to detect, diagnose, and fix JavaScript memory leaks using Chrome DevTools heap snapshots, allocation timelines, and common leak patterns.