jsguides

The JavaScript Performance API

The Performance API gives you precise JavaScript performance data: high-resolution timing, 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, which is 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, so 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"] });

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.

Measure what users feel

The most useful timings are often the ones that map to a user-visible pause. Focus on page load milestones, resource spikes, and interactions that feel sluggish. That turns raw numbers into decisions about what to fix next instead of turning the page into a spreadsheet of labels.

Compare before and after

A measurement only matters when you can compare it to another run. Take one baseline, change a single thing, and measure again under similar conditions. That habit keeps you from chasing noise. If the second run is faster, you know the change helped. If not, you can roll back without guessing.

Keep observers focused

Performance observers are most useful when they watch a narrow set of entry types. A small filter is easier to interpret and less likely to bury the important entry in a long stream of noise. Once you know what you are watching, disconnect the observer so it does not keep collecting data you no longer need.

Treat timing as data, not truth

A timing sample can be distorted by cache state, background activity, garbage collection, or throttling. That does not make the measurement useless; it just means you should read it as one point in a pattern. Run the same test more than once and look for a trend before drawing a hard conclusion.

Keep benchmarks honest

Good timing data comes from repeatable tests. Measure the same action a few times, clear any marks you no longer need, and keep the environment as steady as you can. That habit matters more than any single number because it helps you compare changes instead of reacting to one noisy run.

Track what changed

When a timing chart gets slower or faster, connect the result to the code change that caused it. A performance number is most useful when you can say which path it describes and why it moved. That turns the API into a practical feedback loop instead of a pile of labels.

Measure a single change at a time

If you change several things at once, the result becomes hard to interpret. Keep the test focused on one path so you can explain the outcome with confidence. That habit makes performance work more like debugging and less like guessing.

Keep the context stable

Browser state, tab visibility, and cache hits all affect what the numbers mean. Try to compare runs under similar conditions so the difference comes from the code, not from the environment. A stable context makes the measurements easier to trust.

Keep one result in context

A single measurement only helps if you can explain what changed around it. Note the input size, browser state, and code path so the number means something later. That turns the metric into a comparison point instead of a stray fact.

See Also