Real User Monitoring — Core Web Vitals
Integrated the web-vitals library to capture LCP, INP, CLS, FCP, and TTFB from real browser sessions, beaconing to /api/cwv → D1 → KV snapshot pipeline with p50/p75/p95 percentiles and indefinite retention.
Why RUM instead of synthetic testing
Lighthouse and PageSpeed Insights give you a single synthetic measurement in a controlled environment. Real User Monitoring captures what actual visitors experience — on their devices, their connections, their browser load states. For Core Web Vitals, Google uses field data (CrUX), not lab data, to determine pass/fail thresholds. RUM matches what Google actually measures.
The web-vitals library (Google’s own) reports the same metrics Chrome uses for CrUX, so my data is directly comparable to what affects search rankings.
Data flow
web-vitalslibrary initializes on page load (client-side, ~2KB gzipped)- Metrics fire as they become available: FCP and LCP on paint, CLS on layout shift, INP on interaction, TTFB immediately
- Each metric beaconed via
sendBeaconto/api/cwv— non-blocking, fires even on page unload - Worker handler writes to D1
cwv_log: metric name, value, page path, timestamp cwv-snapshotjob runs on schedule: reads new rows incrementally, computes p50/p75/p95, writes to KV/page-speed/dashboard reads from KV only — no D1 on page load
Why sendBeacon
fetch from an unload handler is unreliable — browsers cancel in-flight requests when navigating away. sendBeacon is designed exactly for this use case: it queues a small payload to be delivered asynchronously, survives page unload, and doesn’t block the next navigation. The cost is you can’t inspect the response, but for analytics this doesn’t matter.
Percentile choice: p75
The p50 (median) experience is often misleading for performance — it hides the tail. Google uses p75 as the threshold for Core Web Vitals pass/fail: your 75th percentile user needs to have a Good experience. I track p50, p75, and p95 but use p75 as the primary metric to match Google’s standard.
Reservoir sampling
Raw CWV values accumulate unboundedly if kept forever. I use reservoir sampling: each metric’s raw values are capped at 500 samples. New values replace random existing values once the cap is hit. This gives statistically valid percentile estimates with bounded storage. The error on p75 from a 500-sample reservoir is negligible for practical purposes.
Privacy
No IP address. No cookies. No session ID. No user identifier of any kind. The only data collected is the page URL and the metric value. No cross-page tracking is possible with this data. This is noted explicitly in the privacy policy.
CWV thresholds
| Metric | Good | Needs Improvement | Poor |
|---|---|---|---|
| LCP | ≤ 2.5s | 2.5s – 4.0s | > 4.0s |
| INP | ≤ 200ms | 200ms – 500ms | > 500ms |
| CLS | ≤ 0.1 | 0.1 – 0.25 | > 0.25 |
| FCP | ≤ 1.8s | 1.8s – 3.0s | > 3.0s |
| TTFB | ≤ 800ms | 800ms – 1800ms | > 1800ms |
Per-path data is available for the top 100 paths by traffic volume. Data is retained indefinitely — the full site lifetime is visible in the /page-speed/ dashboard.