JavaScript Performance Timing Table
| Piece | What it does | Field note |
|---|---|---|
performance.now() | High-res clock | Microsecond-precision monotonic - never Date.now for timing |
performance.mark(name) | Timestamp a moment | Named points in the timeline - visible in devtools tracing |
performance.measure(a, b) | Duration between marks | Feeds the User Timing panel - durations with names |
getEntriesByType('measure') | Read them back | Programmatic access - report your own KPIs |
timeOrigin | The epoch anchor | now() is relative to navigation start - absolute times need it |
Monotonic guarantee | No clock jumps | NTP adjustments never make now() go backwards - Date can |
performance.toJSON | Serialize the timeline | Ship the entries to your telemetry pipeline |
Responsiveness metrics | The modern outputs | INP-style durations measured like a user feels them |
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
- Instrument the real operation: performance.mark('parse-start'); doWork(); performance.measure('parse', 'parse-start') - named spans land in the profiler with zero infrastructure.
- Report your own KPIs: performance.getEntriesByType('measure').filter(m => m.name === 'parse') - aggregate p50/p95 in-app and ship to telemetry.
- 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.