JavaScript Performance API: Precise Runtime Measurement with performance.now() and PerformanceObserver
AI generated
JS
() =>
JavaScript · Performance API · Web Vitals
JavaScript Performance API
Precise Runtime Measurement with performance.now() and PerformanceObserver

Date.now() delivers milliseconds with a precision that is not sufficient for serious performance measurement, and falls into the trap of system clock synchronization. The JavaScript Performance API provides sub-millisecond timestamps, structured marks and measures, a non-blocking observer mechanism, and direct access to Web Vitals without an external library.

12 min read performance.now · Mark · Measure · PerformanceObserver · Long Tasks All modern browsers · Node.js 16+

1. Why Date.now() is unsuitable for performance measurement

The most common approach for runtime measurement in JavaScript is the difference between two Date.now() calls. The problem: Date.now() returns the number of milliseconds since the Unix epoch and is tied to the system clock. The system clock can change, through NTP synchronization, manual adjustment, or daylight saving time transitions. This means a time interval measured with Date.now() can be negative if the system clock is set back during the measurement. For short time spans, Date.now() also only delivers integer milliseconds without sub-millisecond resolution.

The JavaScript Performance API solves both problems. performance.now() is based on a monotonic clock that is guaranteed to never run backward, independent of system clock changes. The returned number is a floating-point value with sub-millisecond precision, rounded to 0.1 ms in browsers for security reasons, and with full CPU resolution in non-security contexts (for example Node.js). The starting point is the page's navigation start, not the Unix epoch, which makes the values smaller and easier to read.

2. performance.now(): sub-millisecond precision and monotonicity

performance.now() returns the number of milliseconds since the navigation start of the current page, as a floating-point number with up to five decimal places. The value is monotonic: it never decreases, even with system clock changes. That makes it the reliable foundation for all Performance API measurements. The difference between two performance.now() calls is always non-negative and reflects the actual elapsed computation and wait time.

A common misunderstanding: performance.now() does not measure CPU time, but wall-clock time, including wait times for I/O, network, and timers. Anyone who wants to measure only CPU time needs Worker Threads or server-side measurements. In the browser, wall-clock time is usually what you want to measure: the time a user actually waits for a response. The Performance API delivers exactly that number, precisely and without system clock dependency.


// 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() and performance.measure()

The User Timing API is the most expressive layer of the Performance API for application code. Calling performance.mark("name") stores a named timestamp in the performance buffer. Calling performance.measure("name", "start-mark", "end-mark") stores the difference between two marks as a PerformanceMeasure entry, which is automatically visible in browser DevTools under the "Timings" timeline section. This means your own measurements are visualized directly alongside browser-internal timings such as navigation and resource loading.

A key advantage over a manual performance.now() difference approach: marks and measures are stored in a separate buffer and can be retrieved at any time with performance.getEntriesByType("mark") and performance.getEntriesByType("measure"), even after the measured code section has finished, for example in an error handler or a reporting callback. With performance.clearMarks() and performance.clearMeasures(), stale entries can be removed from the buffer to avoid memory leaks in long-running applications.


// 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: asynchronous and non-blocking measurement

The PerformanceObserver is the modern, reactive interface to the Performance API. Instead of polling a buffer (getEntriesByType()), you register a callback that is invoked asynchronously as soon as new performance entries of a given type become available. This is especially important for metrics that occur continuously, such as Resource Timing, Long Tasks, and Layout Shifts, where a polling-based approach would either arrive too late or waste CPU time unnecessarily.

The PerformanceObserver does not block the main thread: the callback runs as a microtask after the current event loop tick. That makes it ideal for production monitoring that must not affect the user experience. A single observer can watch multiple entry types at once (observe({ type: "mark" }), observe({ type: "measure" })). Calling observer.disconnect() stops the observer when no further entries are expected.

5. Resource Timing: analyzing network load times

The Resource Timing API is part of the Performance API and automatically captures detailed timing information for every resource a page loads: scripts, stylesheets, images, fonts, and XHR/fetch requests. Each entry provides timestamps: startTime, fetchStart, domainLookupStart, connectStart, requestStart, responseStart, responseEnd. From these timestamps you can calculate DNS lookup time, TCP connection time, TTFB (Time to First Byte), and download time.

The Resource Timing API is particularly valuable for identifying network bottlenecks without external monitoring tools. If a script takes 800 ms to load, the Resource Timing API shows whether the problem lies in DNS resolution, the TCP connection, TTFB, or the actual download. That enables targeted optimizations: CDN setup, preconnect hints, HTTP/2 multiplexing, or caching strategies, driven by data based on real user data, not lab values.


// 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: detecting jank and blocking time

A "Long Task" is defined as any JavaScript execution that blocks the main thread for longer than 50 ms. Long tasks prevent the browser from responding to user input, animations running smoothly, and scroll interactions feeling responsive, a phenomenon known as "jank". The Long Tasks API within the Performance API makes it possible to automatically detect and log such blocking code sections without developers having to manually instrument every code section.

Every long task entry contains an attribution array that provides information about the container (document, iframe, worker) in which the long task ran. Combined with User Timing marks, you can make precise statements: "This 120 ms long task ran during our loadUserData measure." That is the foundation for evidence-based performance optimization instead of guessing from DevTools screenshots.

7. Measuring Web Vitals directly with the Performance API

Web Vitals, LCP (Largest Contentful Paint), FID (First Input Delay), CLS (Cumulative Layout Shift), are the core metrics for web user experience defined by Google. All three can be measured directly via the Performance API, without needing to include the official web-vitals library. LCP is captured via the PerformanceObserver with type: "largest-contentful-paint". CLS with type: "layout-shift". FID with type: "first-input". This enables Real User Monitoring (RUM) directly in your own JavaScript code.

A complete RUM setup with the Performance API sends these metrics asynchronously to an analytics backend, via navigator.sendBeacon(), which is guaranteed to send even when the page is being unloaded. This is the core of Google Search Console, Lighthouse, and Chrome UX Report: real user data, measured in a real browser, via the natively built-in Performance API. Anyone using the web-vitals library can significantly reduce bundle weight with their own lean implementation built on the Performance API.

API Purpose Entry Type Key Property
performance.now() Monotonic timestamp (none) Sub-ms, never negative
User Timing Name code sections mark, measure DevTools integration, buffer query
Resource Timing Network load times resource DNS, TCP, TTFB, download
Long Tasks API Detect jank longtask duration > 50 ms, attribution
Layout Instability Measure CLS layout-shift value, hadRecentInput

8. Performance API in Node.js: perf_hooks and Worker Threads

In Node.js, the Performance API is available via the built-in module node:perf_hooks. performance.now() and PerformanceObserver work there with the same semantics as in the browser, but with higher precision, without the 0.1 ms security cap that browsers introduce for timing-attack protection. That makes Node.js-side measurements with the Performance API especially precise for micro-benchmarks and algorithm comparisons.

In Worker Threads, the Performance API is fully available. Each worker has its own time origin, relative to the worker's start. This enables independent runtime measurement in each thread, without measurements from the main thread and worker threads affecting one another. For benchmarks in Node.js, performance.timerify(fn) is also recommended, which transforms a function into a measured variant and automatically creates a function entry in the performance buffer for each call.


// 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 Optimization and Real User Monitoring

Systematically improve Web Vitals and load times?

We implement Real User Monitoring with the native Performance API, identify Long Tasks and network bottlenecks, and deliver concrete optimization measures for LCP, CLS, and FID.

RUM Setup

Native Performance API RUM without external library: Web Vitals, Long Tasks, Resource Timing

Performance Audit

Analysis of LCP, CLS, TTFB, and Long Tasks based on real user data

Optimization

Concrete measures: preconnect, code splitting, CDN, caching, and bundle optimization

10. Summary

The JavaScript Performance API is the most complete native tool for runtime measurement in the browser and in Node.js. performance.now() delivers monotonic sub-millisecond timestamps without system clock dependency. User Timing with performance.mark() and performance.measure() makes your own code sections visible, directly in DevTools. The PerformanceObserver enables reactive, non-blocking capture of Resource Timing, Long Tasks, and Web Vitals. The Long Tasks API identifies sources of jank without manual instrumentation.

Combining these tools builds a complete Real User Monitoring system without an external library, delivering real user data for LCP, CLS, and FID. That is the foundation for evidence-based performance optimization, not based on lab values from Lighthouse, but based on Performance API data from real users under real network conditions.

JavaScript Performance API: the essentials at a glance

performance.now()

Monotonic, sub-millisecond, navigation-relative. Never negative, no system clock dependency. The basis of all performance measurement.

User Timing

performance.mark() and performance.measure() make code sections visible in DevTools and retrievable from the buffer.

PerformanceObserver

Reactive, non-blocking. Watches Resource Timing, Long Tasks, and Web Vitals asynchronously, ideal for RUM in production.

Long Tasks API

Detects jank: JS execution > 50 ms. Attribution array shows which script is blocking. No manual instrumentation needed.

11. FAQ: JavaScript Performance API

1What is the JavaScript Performance API?
Native API for runtime measurement: performance.now(), User Timing, Resource Timing, Long Tasks API, and PerformanceObserver, without an external library.
2Why performance.now() instead of Date.now()?
Monotonic, sub-ms precision, independent of the system clock. Date.now() can change with NTP sync and only delivers integer milliseconds.
3What is PerformanceObserver?
Reactive, non-blocking observer for performance entries. Watches resource, longtask, measure, layout-shift, and more asynchronously.
4What is a Long Task?
JS execution > 50 ms on the main thread. Blocks animations and input (jank). Long Tasks API reports automatically via PerformanceObserver.
5Web Vitals without a library?
Yes. LCP, CLS, FID directly via PerformanceObserver with the corresponding types. The web-vitals library is convenient, but not required.
6What is User Timing?
performance.mark() sets timestamps, performance.measure() calculates the difference. Both visible in DevTools and retrievable from the buffer.
7How precise is performance.now() in the browser?
0.1 ms in the standard context (Spectre protection). Full microsecond precision in cross-origin isolated contexts (COOP + COEP). Node.js: nanoseconds.
8What is Resource Timing?
Automatic timing data for all resources: DNS, TCP, TTFB, download. No manual instrumentation needed.
9Performance API in Node.js?
import { performance, PerformanceObserver } from 'node:perf_hooks'. Same API, higher precision. Also: timerify() and createHistogram().
10navigator.sendBeacon for RUM?
sendBeacon guarantees delivery even when the page is being unloaded. For RUM systems sending Performance API data, more reliable than fetch in the unload event.