Core Web Vitals tracking requires more than synthetic tests. Learn to build robust performance observability using the Beacon API for accurate RUM data.
We’ve all been there: the synthetic dashboard shows green, but the Slack channel is blowing up with users complaining about a "sluggish" site. Relying on lab data is like checking the weather report while sitting in a windowless bunker; it tells you what the environment should be, not what your users are actually experiencing.
To fix this, I recently overhauled our RUM (Real User Monitoring) strategy. We needed to move away from unreliable XMLHttpRequest pings—which often get canceled when a user navigates away—and move toward a more robust, user-centric approach. Here is how we implemented a production-ready observability pipeline using the Beacon API and standard Web Vitals.
When you track metrics like Largest Contentful Paint (LCP) or Interaction to Next Paint (INP), you’re often sending data at the exact moment a user is leaving the page or clicking a link. If you use a standard fetch or XHR request, the browser will likely terminate those requests as the document unloads.
The Beacon API, specifically navigator.sendBeacon(), was designed for this. It queues an asynchronous, non-blocking POST request that the browser guarantees to attempt even if the page is being closed. It’s fire-and-forget, exactly what you need for telemetry.
JAVASCRIPT// A simple wrapper to report metrics function reportMetric(metric) { const data = JSON.stringify({ name: metric.name, value: metric.value, id: metric.id, }); // The browser ensures this is sent even if the user closes the tab navigator.sendBeacon(CE9178">'/analytics/performance', data); }
When we first set up our telemetry, we made the mistake of sending everything. Every mouse move, every scroll event, every tiny layout shift. Our database choked, and our observability costs skyrocketed. We learned the hard way that you need to sample your data and prioritize your metrics.
To bridge the gap between client-side issues and server-side performance, we now use Performance observability: Bridging Backend Bottlenecks and Core Web Vitals to ensure we aren't just looking at frontend numbers in a vacuum. If a user reports a high LCP, I want to know if it was a slow network or a slow server response.
When you're diving into these metrics, don't forget that Core Web Vitals Optimization: Connecting Backend Latency to RUM is key to understanding the full lifecycle of a request. By injecting Server-Timing headers, you can correlate your backend processing time with the metrics captured by the Beacon API.
Don't reinvent the wheel. Use the web-vitals library. It’s the industry standard for a reason. Instead of writing custom event listeners for layout-shift (which is a headache), import the library and pipe it into your beacon handler.
JAVASCRIPTimport { onLCP, onFID, onCLS } from CE9178">'web-vitals'; function sendToAnalytics(metric) { const body = JSON.stringify(metric); // Use navigator.sendBeacon for reliable delivery navigator.sendBeacon(CE9178">'/api/v1/metrics', body); } onLCP(sendToAnalytics); onFID(sendToAnalytics); onCLS(sendToAnalytics);
One caveat: onCLS can report multiple times as the page layout stabilizes. You might want to buffer these or just send the final value to avoid bloating your logs with intermediate states. We found that sending just the final delta is usually enough for most reporting tools.
Initially, we tried to build a custom buffer that would batch all metrics into a single request every 5 seconds. It worked fine until we ran into mobile devices with aggressive background-process throttling. The batching logic would often fail to execute because the main thread was too busy during navigation.
We switched to a "send-on-event" model using the Beacon API. While it creates more individual requests, the overhead is negligible compared to the reliability of the data we get back. It’s roughly 1.8x more data points than we had before, but the accuracy is finally high enough to trust for decision-making.
Tracking Core Web Vitals is only half the battle. If your LCP is consistently poor in a specific region, you need to know if it’s an edge-caching issue or a database bottleneck. By combining the Beacon API with the Server-Timing API for INP Optimization: Debugging Backend Latency, you can start to see the full picture.
I’m still not entirely sold on our current sampling rate of 10%. Sometimes, rare performance issues only show up in the bottom 5% of traffic, and we might be missing them. Next quarter, I’m planning to implement a "high-fidelity" sampling mode for users who report slow interactions, triggered by a small PerformanceObserver threshold.
Q: Does the Beacon API support custom headers?
A: No, navigator.sendBeacon is limited. You cannot set custom headers like Authorization or Content-Type. You have to handle authentication via cookies or tokens in the URL query parameters.
Q: Should I use Beacon API for critical business data? A: Absolutely not. It is fire-and-forget. If the request fails, you won't know, and you won't get a retry. Keep it strictly for telemetry and observability.
Q: How does this affect my site's performance? A: Because Beacon requests are asynchronous and handled by the browser's networking stack outside the main thread, they have virtually zero impact on your users' experience.
Building a robust performance observability pipeline is less about the tools and more about the discipline of data collection. Start small, use the standard web-vitals library, and rely on the Beacon API for your transport layer. You'll stop guessing about why your site feels slow and start having the data to prove exactly where the bottlenecks live.
Learn to eliminate critical request chains and boost Core Web Vitals. Discover how precise resource prioritization and preload scanning improve load times.
Read moreMaster Server-Side Rendering resilience by preventing hydration mismatches and layout shifts. Learn how streaming and atomic updates stabilize Core Web Vitals.