JavaScript User Timing Table

PieceWhat it doesField note
performance.now()The clockmonotonic, high-resolution, immune to clock steps - one clock per figure, never mix with Date.now() (wall time steps backward)
performance.mark(name)The breadcrumbnanosecond-cost named stamp into the performance timeline; DevTools shows it on the User Timing track
performance.measure(name, start, end)The durationtwo marks become a named measure; missing mark names throw SyntaxError - silent catches here empty your dashboard
the detail payloadThe context{detail: {variant, hits, cohort}} rides the entry - timing plus context in one entry is what turns debugging into analytics
PerformanceObserverThe collectorobserve type measure with buffered: true to replay entries that fired before attach - fixes the bootstrap race every load
the RUM beaconThe ship-outbatch and send on visibilitychange-to-hidden via sendBeacon/keepalive; sample if volume demands it
clearMarks/clearMeasuresThe cleanupthe timeline buffer caps (~150 marks/~250 measures) and SPAs leak entries per route - clear on transition or stream continuously
the naming disciplineThe taxonomyproduct-language names (catalog-load, search) survive refactors; name churn silently breaks every historical comparison
Reference: the MDN User Timing API. Named high-resolution stamps over a monotonic clock: marks, measures between them, context payloads riding each entry - your instrumentation as first-class citizens of the same timeline the browser reports on.
Bottom line: measure from marks, never mixed clocks, and name by product meaning. The four ways timings lie: Date.now() contamination, async gaps that measure the scheduler, first-call JIT inflation (report medians, never first samples), and buffer caps that silently bias long sessions. Ship three fields per event - name, duration, detail - through one buffered observer and a visibilitychange beacon.
Related tools: the performance timing table (the browser-native timings on the same axis), the event loop table (the scheduling that inflates async measures), the observer table (the PerformanceObserver family), the visibility table (the hidden moment that times the beacon), and the beacon table (the send that survives page death).

User Timing is the API for answering one question with precision: how long did THIS take? performance.mark(name) drops a timestamped breadcrumb into the performance timeline, performance.measure(name, startMark, endMark) turns two breadcrumbs into a named duration, and both are readable as entries, visible in the DevTools performance panel, and observable from PerformanceObserver - which makes your timings first-class citizens of the same timeline the browser itself reports on.

Bottom line: the clock is performance.now() - monotonic, high-resolution (microsecond-ish, deliberately coarsened slightly against timing attacks), immune to system clock changes and timezones - and marks and measures are just named stamps over it. The measurement rule that keeps numbers honest: measure from marks or from explicit timeOrRouteCall parameters, never mix performance.now() arithmetic with Date.now() in the same figure (Date is wall-clock: NTP can step it backward mid-measurement), and name things by what they mean to the product (parse-catalog-json) not to the implementation (fn2-timer).

The underrated half is the detail payload: both mark and measure accept a second argument with a detail field - an object you can retrieve from the entry later - so a single measure can carry which variant ran, which endpoint responded, which cohort the user is in. Timing plus context in one entry is what turns console debugging into analytics: PerformanceObserver picks your measures up in a RUM beacon with the who and the why attached, no separate instrumentation pipeline.

How to use

  1. Stamp the moments: performance.mark('app-start'); performance.mark('catalog-fetched'); - marks cost nanoseconds, take a name and optional {detail}, and land in the performance timeline where DevTools shows them.
  2. Measure between marks: performance.measure('catalog-load', 'app-start', 'catalog-fetched'); - or measure('parse', {start: 'catalog-fetched', duration: 0}) style options with start/end/duration; the result is also returned as a PerformanceMeasure you can read immediately.
  3. Attach context: performance.measure('search', {start: 'q-start', end: 'q-end', detail: {variant: 'fuzzy', hits: 23}}); - detail rides the entry; retrieve later via entry.detail (some engines need getEntriesByName and JSON round-trips for structured clones).
  4. Observe continuously: new PerformanceObserver(list => list.getEntries().forEach(e => beacon(e.name, e.duration, e.detail))).observe({type: 'measure', buffered: true}); - buffered true replays entries that fired before the observer attached, which fixes the bootstrapping race on every page load.
  5. Clean the timeline: performance.clearMeasures('catalog-load') and clearMarks in hot paths and single-page transitions - the timeline buffer holds entries for the page life, and unbounded marks in an SPA become a slow leak that DevTools and observers both pay for.

Frequently asked questions

Why use marks and measures instead of just subtracting performance.now() calls?

Subtraction gives you a number; the timeline gives you an instrument. Six concrete upgrades. Marks and measures appear in the DevTools performance panel (User Timing track) alongside frames, network and layout - your instrumentation sits in context with what the browser was doing, which is where the why of a slow operation lives. They are named, so dashboards and alerts reference stable product-language keys instead of magic constants scattered through the code. They are entries: PerformanceObserver, getEntriesByType and buffered replay come free, which is the entire RUM collection story. They carry detail payloads, so one measure answers duration plus variant plus cohort. They survive refactorings better - timing code drifts out of sync with the code it times, while a named measure pair moves with the function. And they compose with the browser timings on the same axis: your mark sits next to navigationStart-derived paint timings, so catalog-fetched is comparable against first-contentful-paint with no clock juggling. Subtraction is fine for a quick debug; anything that reaches a dashboard deserves the timeline.

What are the classic ways these measurements lie?

Four recurring ones. Wall-clock contamination: mixing Date.now() into the figure - Date steps with NTP and timezone; performance.now() is monotonic, so one clock per figure, ever. Async gaps: measuring start to end across an await chain measures scheduling too - a 'db-query' measure that includes a stalled microtask queue reports storage problems that are actually main-thread congestion; mark immediately around the awaited call, not around the whole handler. First-call inflation: the first measure of any code path pays JIT warmup, cache fills and lazy module init - report the median of many runs, never the first sample, or you are measuring the compiler. And buffer truncation: default buffers cap (150 for marks, 250 for measures in Chrome-family) - long sessions silently drop early entries, which biases every getEntriesByName analysis toward recent activity; either observe-and-ship continuously or raise the buffer and know the cap. There is a fifth micro-lie: coarsened clocks round to fractions of a millisecond by design - durations under ~0.1ms are noise; do not report sub-tick precision as if it were measured.

How does this fit with RUM and analytics - what do I actually ship?

Ship measures as events with three fields and one discipline. Fields: name (the stable product-language key), duration (the number), detail (the dimensions - variant, endpoint, cohort). The discipline: collect via a single PerformanceObserver with buffered: true, batch and beacon on visibilitychange-to-hidden (the only reliable send moment - sendBeacon or fetch keepalive), and sample if volume demands it (the observer sees every measure; beaconing every one of them from every user is a self-inflicted outage). What to instrument: the operations your product promises - search latency, checkout step durations, offline-sync catch-up - plus one end-to-end measure per critical path, because executives read the end-to-end number and engineers debug the step numbers. What not to instrument: framework internals and render loops (the browser's own long-task and paint timings already cover those), and anything you cannot name in product language - if the dashboard cannot say it in a sentence, the measure will be noise within a sprint. Finally, version the names: measure-name churn silently breaks every historical comparison; treat the taxonomy like an API.

Do marks and measures cost anything, and where are the edges?

Almost nothing where it matters, with three real edges. Cost: a mark is a nanosecond-scale stamp and a measure a trivial entry append - you can afford them in production at human-scale frequencies; the cost is in the observers and beacons you attach, not the entries. Edge one: buffer growth in SPAs - every navigation adding a dozen marks is a slow leak; clearMeasures/clearMarks on route transitions, or accept the default caps and stream out continuously. Edge two: name collisions - marks share one global namespace per document, so two libraries marking 'start' corrupt each others measures; prefix with your module name. Edge three: cross-realm and worker isolation - performance timelines are per-realm, so a worker marks itself, never your window timeline; postMessage the durations or observe inside the worker and beacon from there. And one trap at the API edge: measure() with nonexistent mark names throws SyntaxError (not a silent null) - which is correct behavior, but in try/catch-heavy startup code it can be swallowed and leave your dashboard mysteriously empty; fail loudly on missing marks in development.

Related tools