Measuring What Standard Metrics Miss
Standard, universal performance metrics give you a shared baseline. They let you gauge how real users experience the web overall, compare your site against a competitor, and wire useful data into your analytics without writing custom instrumentation.
That baseline, though, only goes so far. To capture the complete experience on your own site, you often need to measure more than those universal metrics allow. Custom metrics exist for exactly that job: tracking the parts of your user experience that are unique to your implementation.
- The time a single-page app takes to complete a route transition from one view to the next.
- How long a page takes to render data fetched from a database for logged-in users.
- The time a server-side-rendered app needs to hydrate.
- The cache hit rate for resources requested by returning visitors.
- Click or keyboard event latency inside a game.
Custom metrics give you the flexibility to track exactly the behavior that matters for your specific product, rather than relying on general signals that may not reflect your user's real constraints.
A Measure of Experience, Not Just the Page Load
The key conceptual shift begins with acknowledging that a user experience does not have to coincide with a browser loading an HTML document. Users can experience a page after navigation starts, during a replay or an animation, or through an interaction that never triggers a page load at all.
Instead of framing every metric around document load, you should define measurements in terms of the events that your users actually notice. Each of those events is a moment in time you can capture with a timestamp from the browser. Once you have the raw timestamps, you can combine them to represent a complete user journey, including periods that precede or follow the initial page rendering.
Captured timestamps can be sent back as event-based data to your analytics endpoint. This gives you the flexibility to treat each milestone as its own event or to assemble individual timings into a coherent, narrative timeline of the experience.
The approach lends itself equally to measuring specific sub-tasks during the page lifetime: when a particular script finishes, when a section appears on screen, or when a user's primary interaction begins. These smaller, bespoke measurements complement—rather than replace—the universal metrics, filling in the gaps where generic performance numbers leave you guessing.
Choosing the right measurement API
For years, developers had to rely on improvised techniques to detect performance problems. A common workaround was running a requestAnimationFrame loop and measuring frame-to-frame deltas to infer when the main thread was blocked by long tasks. Such tricks have a fundamental flaw: they consume power and CPU, so the act of measuring degrades the very performance being measured.
Modern browsers now expose purpose-built APIs that collect this data passively. The most important rule when adding custom metrics is to avoid measurement techniques that introduce their own cost. Where possible, use the standard APIs below instead of hand-rolled detection loops.
PerformanceObserver: the entry point
All the APIs covered here deliver data through the PerformanceObserver interface. Rather than polling, you subscribe to performance event types, and callbacks fire during idle periods so they don’t interfere with page work.
To start collecting entries, create an observer with a callback and call observe() with the entry types you care about. In newer browsers, the static PerformanceObserver.supportedEntryTypes property lists what’s available.
A practical detail: observers only see events that occur after creation by default. If you lazy-load your analytics code and need prior entries, pass buffered: true to observe(). The browser then replays historical entries from its internal buffer on the first callback invocation.
Avoid the older getEntries(), getEntriesByName(), and getEntriesByType() methods on the performance object. They don’t provide live updates, and newer entry types such as largest-contentful-paint aren’t exposed through them at all. Except when supporting Internet Explorer, prefer PerformanceObserver.
User Timing
The User Timing API is a general-purpose tool for time-based metrics. Call performance.mark() to stamp a point in time and performance.measure() to record the duration between two marks. While Date.now() and performance.now() offer similar capabilities, User Timing integrates with browser tools—Chrome DevTools renders these measurements in the Performance panel—and analytics providers can automatically capture and report them. Subscribe to measure entries to read the results.
Long tasks and long animation frames
The Long Tasks API reports any task that occupies the main thread for more than 50 milliseconds. High-level metrics like Time to Interactive and Total Blocking Time are built on top of it. Observe the longtask entry type to see when render-blocking work occurs.
The newer Long Animation Frames API is a revised take that targets long frames (again over 50 ms) rather than tasks. It improves on the Long Tasks API with better attribution and broader coverage of problematic delays. Listen for long-animation-frame entries to use it.
Custom element rendering times
Largest Contentful Paint identifies the single biggest paint, but sometimes a specific image or text block matters more. The Element Timing API handles that case. Add the elementtiming attribute to a target element, then observe the element entry type. LCP itself is implemented on top of this same API.
Interaction and event timing
Interaction to Next Paint (INP) is derived from the Event Timing API, which tracks the full lifecycle of click, tap, and keyboard events. The useful timestamps include:
startTime: when the browser receives the event.processingStart: when event handlers begin executing.processingEnd: when synchronous handler code completes.duration: from event receipt to the next painted frame after handlers finish, rounded to 8 ms for privacy.
These values let you measure delays that standard paint metrics overlook, including time spent waiting on long handler chains or queued tasks.
Resource, navigation, and server insights
The Resource Timing API tracks how individual page resources were fetched. It exposes data beyond pure timing, including the fetch mechanism (initiatorType), transfer protocol (nextHopProtocol), encoded and decoded sizes, and transferSize. Comparing transferSize with the encoded body size reveals whether a response came from cache, which supports cache-hit-rate metrics for repeat visits.
The Navigation Timing API is the sibling for navigation requests. Its navigation entries carry extras like DOMContentLoaded and load event times. The responseStart timestamp is the standard source for Time to First Byte. For service worker scenarios, the delta between responseStart and workerStart quantifies the time the browser spends starting the worker thread before it can intercept fetch events.
Server Timing for backend measurements
Server-side latency is often invisible to frontend monitoring. The Server Timing API moves request-specific timing data from server to browser via the Server-Timing response header, which is useful for debugging slow database lookups or correlating backend delays with user-experience metrics. The data appears on both resource and navigation entries and remains the only standard way for third-party analytics to connect server performance with business metrics.



