JavaScript Performance Timing Table

PieceWhat it doesField note
performance.now()High-res clockMicrosecond-precision monotonic - never Date.now for timing
performance.mark(name)Timestamp a momentNamed points in the timeline - visible in devtools tracing
performance.measure(a, b)Duration between marksFeeds the User Timing panel - durations with names
getEntriesByType('measure')Read them backProgrammatic access - report your own KPIs
timeOriginThe epoch anchornow() is relative to navigation start - absolute times need it
Monotonic guaranteeNo clock jumpsNTP adjustments never make now() go backwards - Date can
performance.toJSONSerialize the timelineShip the entries to your telemetry pipeline
Responsiveness metricsThe modern outputsINP-style durations measured like a user feels them
Reference: the MDN Performance API reference. The Performance API is the honest clock: performance.now() is MONOTONIC microsecond precision (Date.now can jump backwards under NTP - timing code must never touch it), and mark/measure turn timing into named, devtools-visible spans. Bottom line: mark at the start of the operation, measure at the end, and the entries become programmatic KPIs (getEntriesByType) plus devtools User Timing lanes - your app measures itself in the same vocabulary the profiler uses. Related tools: console table (time/timeEnd, the coarse sibling), observer table (PerformanceObserver, the metrics stream), and event loop table (what the clock actually measures between).

The Performance API is the honest clock: performance.now() is MONOTONIC microsecond precision - it never jumps backwards under NTP adjustments, never skips under DST, and Date.now (wall time, millisecond, jumpable) must never appear in timing code.

Bottom line: mark and measure turn timing into named, devtools-visible spans - performance.mark at the start, performance.measure(name, startMark) at the end, and the entries appear in the User Timing lane AND read back programmatically via getEntriesByType: your app measures itself in the same vocabulary the profiler uses.

The honest part: now() is relative to timeOrigin (navigation start), so cross-page or cross-day comparisons need the origin added back. And browsers reduce the clock's precision for security (Spectre mitigations) - expect tens of microseconds, not exact nanoseconds.

How to use

  1. Instrument the real operation: performance.mark('parse-start'); doWork(); performance.measure('parse', 'parse-start') - named spans land in the profiler with zero infrastructure.
  2. Report your own KPIs: performance.getEntriesByType('measure').filter(m => m.name === 'parse') - aggregate p50/p95 in-app and ship to telemetry.
  3. Time precisely without marks: const t0 = performance.now(); ...; const ms = performance.now() - t0 - fine for micro-benchmarks inside one function.

Frequently asked questions

Why must timing code never use Date.now()?

Wall time is not a clock for measuring. Date.now returns civil time, which jumps: NTP corrections step it backward or forward by varying amounts, DST changes it, and leap adjustments exist - a 200ms measurement can read as negative or 30 seconds. performance.now() is MONOTONIC (never decreases) and high-resolution - the only property a duration measurement can rely on. The subtler point: Date.now is also only millisecond-precision, so sub-millisecond operations measure as 0 - a real hazard that makes micro-benchmarks lie by rounding.

What do mark and measure buy over manual now() arithmetic?

Names, visibility and aggregation. Manual arithmetic (t1 - t0) gives you a number in a variable; mark/measure give the same duration a NAME, an entry in the timeline, a lane in devtools' User Timing view, and a queryable entry object via getEntriesByType. The practical compounding: instrument the three hot paths once with marks, and every future profiling session shows those named spans next to the flame chart - no correlate-the-log-file archaeology. The entries are also data: aggregate p50/p95 per name in-app, and the app ships its own KPIs with no extra instrumentation layer.

Why is performance.now() relative to timeOrigin, and why was its precision reduced?

Relative by design: the timeline starts at navigation start, which keeps the number small and the same for all entries on the page - absolute wall-clock comparisons across pages or days need timeOrigin added back. The precision reduction is a security answer: high-resolution timers made Spectre-class side-channel attacks possible (tiny timing differences leak secrets), so browsers coarse-grained the clock to tens or hundreds of microseconds - with cross-origin isolation restoring finer resolution. The takeaway: micro-benchmarks measure in the precision you are given, and differences below the floor are noise by mandate.

How do the timing APIs relate to PerformanceObserver?

Pull versus push. performance.mark/measure/now are the recording side - you create entries; getEntriesByType is the pull side - you read them on demand. PerformanceObserver (from the observer family) is the push side: subscribe to entry types (measure, longtask, LCP, event) and receive them as they arrive - the correct shape for telemetry pipelines and live dashboards, since buffering-and-polling misses entries between checks. The standard architecture: marks and measures written by feature code, a PerformanceObserver collecting them for reporting, and devtools showing the same entries for humans - one timeline, three consumers.

Related tools