How to Use the User Timing API: Complete Guide (2026)
What Is the User Timing API?
The User Timing API is a browser-native API that lets you instrument your own application code with named performance marks and measure the duration between them.It’s the standard way to capture custom performance metrics — like how long an “Add to Cart” click takes to render — that aren’t covered by Core Web Vitals like LCP, CLS, or INP.
Key Facts (TL;DR)
- Two core methods:
performance.mark(name)drops a timestamp;performance.measure(name, startMark, endMark)measures the duration between two marks. - High resolution: measurements are accurate to fractions of a millisecond via
DOMHighResTimeStamp. - DevTools integration: marks and measures appear in the Timings lane of Chrome DevTools’ Performance tab — visible without any extra setup.
- Streamable: subscribe with
PerformanceObserver({ entryTypes: ['measure'] })to ship custom timings to your RUM or analytics pipeline. - Why audits care: hand-rolled timing code (e.g., manual
Date.now()diffs) is harder to inspect and ship; the User Timing API is the standardized, lightweight path. - Browser support: available in every modern browser.
PerformanceObserverformeasureentries is also universally supported.
Think of it like a stopwatch with named lap buttons. Core Web Vitals tell you how fast the page loaded; the User Timing API tells you how long any specific slice of your code took — a search query, a chart render, a route transition — and labels it so you can find it later.
Why Custom Timing Matters Beyond Core Web Vitals
Core Web Vitals capture page-level user experience: LCP for largest paint, CLS for layout stability, INP for input responsiveness. They’re excellent at flagging that a page feels slow, but they don’t tell you why a specific feature is slow.
If your “Add to Cart” click takes 800 ms to render, no Core Web Vital will localize the cause. INP will report a high latency for that interaction class, but it can’t tell you whether the cost is in your event handler, your fetch call, your render, or your post-render commit. The User Timing API fills that gap by letting you mark each phase explicitly.
- Application-level visibility: measure exactly the code paths you care about — search, checkout, route transitions, chart rendering.
- Real-user data, not synthetic: marks fire in the user’s actual browser, capturing real-world device, network, and CPU conditions.
- Cross-tool integration: marks appear in DevTools and stream to
PerformanceObserver, so the same instrumentation works for local profiling and production RUM. - INP root-cause analysis: bracket your event handler with marks to break a high INP into “handler ran for X ms, render took Y ms.”
How performance.mark and performance.measure Work
The API is two methods deep. Everything else builds on these.
// 1. Drop a named timestamp
performance.mark('search-start');
// ...do work...
runSearch(query);
// 2. Drop another timestamp
performance.mark('search-end');
// 3. Measure the span between the two marks
performance.measure('search', 'search-start', 'search-end');
// 4. Read the result
const [entry] = performance.getEntriesByName('search', 'measure');
console.log(`search took ${entry.duration.toFixed(1)}ms`);
// -> "search took 142.7ms"Each mark records a high-resolution timestamp keyed by name. Each measure stores a PerformanceMeasure entry with startTime, duration, and the name you gave it. Both live on the browser’s performance timeline alongside built-in entries like navigation and largest-contentful-paint.
Modern object-form syntax
Recent browsers support a richer object form for measure() that accepts numeric times directly — useful when you already have timestamps from another source:
performance.measure('search', {
start: startTime, // number, ms since timeOrigin
end: endTime,
detail: { query, resultCount } // arbitrary metadata
});The detail field travels with the entry, so your analytics layer can read it later without a separate lookup.
How to Inspect User Timings
The same marks and measures show up in three places, depending on what you’re doing:
- Greadme deep scan — surfaces unusually long custom timings on audited pages and ties them back to the affected scripts, with a one-click GitHub PR to fix the slow path.
- Greadme crawler scan — runs the same instrumentation across every indexable page, so you can spot which templates have regressed without manual profiling.
- Greadme AI visibility analyzer — confirms that the pages where you’ve fixed slow custom timings are the ones AI search engines are actually citing.
- Chrome DevTools → Performance → Timings lane — every
markandmeasureis rendered as a labeled bar in the “Timings” track during a profile recording, with no extra configuration. - web.dev / PerformanceObserver — subscribe to
{ entryTypes: ['mark', 'measure'] }at runtime to stream entries to your RUM or analytics provider as they happen.
7 Practical Patterns for the User Timing API
1. Wrap a Synchronous Function
The simplest case: bracket a function call with two marks and measure the gap.
performance.mark('render-chart-start');
renderChart(data);
performance.mark('render-chart-end');
performance.measure('render-chart', 'render-chart-start', 'render-chart-end');2. Time an Async Operation
For promises, mark just before the call and just after it resolves.
performance.mark('search-start');
const results = await fetch('/api/search?q=' + q).then(r => r.json());
performance.mark('search-end');
performance.measure('search', 'search-start', 'search-end');3. Measure an INP Interaction End-to-End
Mark when the event handler starts and when the resulting paint completes (via requestAnimationFrame + a microtask). This isolates handler cost from render cost.
button.addEventListener('click', (e) => {
performance.mark('add-to-cart-handler-start');
addToCart(productId);
performance.mark('add-to-cart-handler-end');
performance.measure('add-to-cart-handler',
'add-to-cart-handler-start',
'add-to-cart-handler-end');
requestAnimationFrame(() => {
requestAnimationFrame(() => {
performance.mark('add-to-cart-painted');
performance.measure('add-to-cart-total',
'add-to-cart-handler-start',
'add-to-cart-painted');
});
});
});4. Stream Measures to Your RUM Pipeline
Use PerformanceObserverto forward every measure to your analytics endpoint as it’s created.
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
navigator.sendBeacon('/rum', JSON.stringify({
name: entry.name,
duration: entry.duration,
startTime: entry.startTime,
detail: entry.detail ?? null,
}));
}
});
observer.observe({ entryTypes: ['measure'] });5. Use the Object Form to Attach Metadata
The detail field travels with the entry, so your dashboard can segment by query, route, or user tier.
performance.measure('search', {
start: startTime,
end: performance.now(),
detail: { query, resultCount: results.length, route: '/products' }
});6. Clear Marks in Long-Lived SPAs
Marks accumulate in memory. For long sessions (single-page apps, dashboards), clear marks you no longer need.
performance.clearMarks('search-start');
performance.clearMarks('search-end');
performance.clearMeasures('search');7. Wrap the API in a Helper
A small helper keeps the call sites clean and makes it trivial to disable instrumentation in non-production builds.
export function timed<T>(name: string, fn: () => T): T {
const start = `${name}:start`;
const end = `${name}:end`;
performance.mark(start);
try {
return fn();
} finally {
performance.mark(end);
performance.measure(name, start, end);
performance.clearMarks(start);
performance.clearMarks(end);
}
}
// Usage:
const results = timed('search', () => runSearch(q));Common User Timing Mistakes
Problem: Reinventing It with Date.now()
What's happening: Code does const t = Date.now(); doWork(); console.log(Date.now() - t); everywhere. The numbers aren’t high-resolution, they don’t show up in DevTools, and they can’t be observed via PerformanceObserver.
Fix: Replace with performance.mark/performance.measure. Same number of lines, but the data lights up in DevTools and streams to RUM automatically.
Problem: Marks Accumulate in Memory
What's happening: A long-lived SPA fires the same marks on every interaction and never clears them. Over a 30-minute session, the performance buffer fills up and gets dropped (along with newer entries you care about).
Fix: Call performance.clearMarks() and performance.clearMeasures() after you’ve consumed the entries, or rely on the helper pattern above to clean up automatically.
Problem: Inconsistent Naming Across the Codebase
What's happening: Different files use search-start, searchStart, and start_search. Dashboards can’t aggregate the data because the names don’t match.
Fix: Standardize a naming convention (e.g., feature:phase) and centralize all performance.mark calls behind a small helper that enforces it.
Problem: Measuring Across Page Navigations
What's happening: Code marks start on page A and tries to measure to end on page B. The performance timeline resets on navigation, so the second page can’t see the first page’s marks.
Fix: Cross-page timings need to be stitched server-side or via sessionStorage. Within a single page (or SPA route transitions that don’t reload), the timeline is continuous and marks survive.
User Timing API vs. Other Performance APIs
The User Timing API is one of several entry types on the browser’s performance timeline. Each answers a different question.
| API / Entry Type | What It Measures | Who Sets It | Best For |
|---|---|---|---|
User Timing (mark, measure) | Custom application-level spans | Your code | Feature-level instrumentation, INP root cause |
Navigation Timing (navigation) | Page load lifecycle (DNS, TCP, response, DOM) | Browser | Page-load breakdown |
Resource Timing (resource) | Per-resource fetch timing | Browser | Diagnosing slow assets, third-party origins |
| LCP / CLS / INP entries | Core Web Vitals | Browser | Page-level UX quality |
Long Tasks (longtask) | Tasks blocking the main thread > 50 ms | Browser | Finding JavaScript that hurts INP |
FAQ
What is the difference between performance.mark and performance.measure?
performance.mark(name) records a single high-resolution timestamp on the timeline. performance.measure(name, startMark, endMark) records a span with a duration between two marks (or two numeric times). You can have many marks and many measures over them.
Where do User Timing entries show up?
In Chrome DevTools, marks and measures appear in the “Timings” lane of the Performance tab during a recording. At runtime, they’re available via performance.getEntriesByType('mark') / 'measure' and stream to any PerformanceObserver watching those entry types.
Does the User Timing API cost anything in production?
The cost is negligible — each mark is a single high-resolution timestamp write. The only practical risk is letting marks accumulate in long-lived sessions; calling performance.clearMarks()after you’ve consumed them keeps memory bounded.
Can the User Timing API help with INP?
Yes — it’s one of the most useful tools for diagnosing high INP. Bracket your event handler with marks, mark again after the next paint, and measure both spans. That breaks an INP regression into “handler ran for X ms, render took Y ms,” which tells you where to optimize.
How do I send User Timing data to a RUM service?
Subscribe with PerformanceObserver({ entryTypes: ['measure'] }) and forward each entry to your endpoint with navigator.sendBeacon. Most production RUM clients ingest name, duration, startTime, and the optional detail object.
Why do performance audits prefer the User Timing API to hand-rolled timing code?
Because it’s standardized: the entries appear in DevTools without extra setup, observers can subscribe to them generically, and the API uses high-resolution timestamps (better than Date.now()). Audit tools flag heavy hand-rolled instrumentation because it’s harder to inspect and harder to ship to production telemetry.
Does the User Timing API affect SEO or AI search engines like ChatGPT and Perplexity?
Indirectly. The API itself isn’t a ranking signal, but it’s a key tool for diagnosing slow interactions that hurt INP, LCP, and overall page experience — all of which feed Google’s ranking signals and the “already ranks well” bias that AI engines like Google AI Overviews and Perplexity use when deciding which pages to cite. Better instrumentation leads to faster pages, which leads to more citations.
Conclusion
The User Timing API is the standardized way to measure exactly the things Core Web Vitals can’t see — your application’s own slow paths. Two methods (performance.mark and performance.measure), one observer (PerformanceObserver), and consistent naming get you everything from on-the-fly DevTools profiling to production RUM with no extra dependencies.
Run a Greadme deep scan to see which interactions on your pages are dominating real-user latency, then add User Timing marks around those code paths to confirm — and fix — the root cause.
