Skip to content
Codeloom
JavaScript

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.

·6 min read · By Codeloom
Intermediate 11 min read

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 }

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() and performance.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.
  • PerformanceObserver reacts 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.