Real User Monitoring — Performance Analytics with RUM
In this tutorial, you will learn how to implement Real User Monitoring (RUM) to collect performance data from actual visitors, aggregate it into meaningful dashboards, and use it to identify regressions and optimization opportunities. Unlike lab tools, RUM captures the true user experience across devices, networks, and geographies. DodaTech uses a custom RUM pipeline to track Core Web Vitals across millions of sessions.
What You Will Learn
- The difference between lab data and field data
- How to collect performance metrics using the Performance API
- How to build a RUM data pipeline from collection to dashboard
- How to identify regressions and prioritize fixes based on real user data
Why It Matters
Lab tools like Lighthouse measure performance in a controlled environment. Real users have different devices, networks, and browser extensions. RUM reveals what users actually experience. DodaTech discovered that 30 percent of users had LCP over 4 seconds even though Lighthouse showed 1.8 seconds, because the lab tests did not account for real-world conditions.
Real-World Use Case
The Doda Browser team used RUM data to discover that users in Southeast Asia had significantly higher LCP due to a CDN edge gap. After adding edge nodes in Singapore and Mumbai, LCP for those regions dropped from 4.2 seconds to 1.9 seconds, a fix that lab testing alone would never have identified.
Prerequisites
You should understand Core Web Vitals metrics and how JavaScript event handling works. Familiarity with Google Analytics or similar analytics platforms is helpful.
Step-by-Step Tutorial
Step 1: Understand Lab vs Field Data
Lab data is collected in a controlled environment with a predefined device and network condition. Field data comes from real users under real conditions.
| Aspect | Lab (Lighthouse) | Field (RUM) |
|---|---|---|
| Environment | Controlled | Real-world |
| Device | Emulated | Actual devices |
| Network | Throttled (simulated) | Real network conditions |
| Cache | Cold by default | Mixed (cold + warm) |
| Geographic | Single location | Global |
| Metrics | LCP, TBT, CLS, SI | LCP, FID, CLS, TTFB |
RUM is essential for metrics that cannot be measured in a lab, such as First Input Delay (FID).
Step 2: Collect Metrics with the web-vitals Library
The web-vitals library provides a simple API for collecting Core Web Vitals in the browser.
<script type="module">
import {onLCP, onFID, onCLS, onTTFB, onFCP, onINP} from 'https://unpkg.com/web-vitals@4/dist/web-vitals.attribution.js?module';
function sendToAnalytics(metric) {
const body = {
name: metric.name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
id: metric.id,
navigationType: metric.navigationType,
url: window.location.pathname,
device: navigator.userAgentData?.mobile ? 'mobile' : 'desktop',
connection: navigator.connection?.effectiveType || 'unknown'
};
// Send via sendBeacon for reliability
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/rum', JSON.stringify(body));
} else {
fetch('/api/rum', {method: 'POST', body: JSON.stringify(body), keepalive: true});
}
}
onLCP(sendToAnalytics);
onFID(sendToAnalytics);
onCLS(sendToAnalytics);
onTTFB(sendToAnalytics);
onFCP(sendToAnalytics);
onINP(sendToAnalytics);
</script>
Expected output: The browser sends a POST request to /api/rum for each metric. The request includes the metric name, value, rating (good/needs-improvement/poor), and context information.
Step 3: Build the Server-Side Ingestion Pipeline
Create a server endpoint that receives RUM data and stores it in a time-series database.
// Express.js RUM ingestion endpoint
const express = require('express');
const app = express();
app.use(express.json());
// In-memory store (use InfluxDB, ClickHouse, or similar in production)
const metrics = [];
app.post('/api/rum', (req, res) => {
const metric = {
...req.body,
timestamp: new Date().toISOString(),
userAgent: req.headers['user-agent']
};
metrics.push(metric);
res.status(204).end();
});
// Aggregation endpoint
app.get('/api/rum/aggregate/:metric', (req, res) => {
const metricName = req.params.metric;
const window = parseInt(req.query.window) || 3600000; // Default 1 hour
const cutoff = Date.now() - window;
const filtered = metrics.filter(m =>
m.name === metricName && new Date(m.timestamp).getTime() > cutoff
);
const values = filtered.map(m => m.value);
const percentiles = (arr, p) => {
arr.sort((a, b) => a - b);
return arr[Math.floor(arr.length * p / 100)];
};
res.json({
metric: metricName,
count: values.length,
p50: percentiles(values, 50),
p75: percentiles(values, 75),
p90: percentiles(values, 90),
p95: percentiles(values, 95),
p99: percentiles(values, 99),
good: filtered.filter(m => m.rating === 'good').length,
needsImprovement: filtered.filter(m => m.rating === 'needs-improvement').length,
poor: filtered.filter(m => m.rating === 'poor').length
});
});
app.listen(3001);
Expected output: Querying /api/rum/aggregate/LCP returns JSON with count, percentiles, and rating distribution for the last hour of data.
Step 4: Build a RUM Dashboard
Visualize the RUM data to track performance trends.
<!DOCTYPE html>
<html>
<head>
<title>RUM Dashboard - DodaTech</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<h1>Real User Monitoring</h1>
<div class="grid">
<div class="card">
<h2>LCP (p75)</h2>
<div id="lcp-chart"><canvas></canvas></div>
</div>
<div class="card">
<h2>CLS (p75)</h2>
<div id="cls-chart"><canvas></canvas></div>
</div>
<div class="card">
<h2>Good / Needs Improvement / Poor</h2>
<div id="rating-chart"><canvas></canvas></div>
</div>
</div>
<script>
// Fetch RUM data and render charts
async function updateDashboard() {
const lcp = await fetch('/api/rum/aggregate/LCP').then(r => r.json());
const cls = await fetch('/api/rum/aggregate/CLS').then(r => r.json());
document.getElementById('lcp-value').textContent = `${(lcp.p75 / 1000).toFixed(2)}s`;
document.getElementById('cls-value').textContent = cls.p75.toFixed(3);
}
setInterval(updateDashboard, 60000);
updateDashboard();
</script>
</body>
</html>
Step 5: Set Up Alerting
Configure alerts to notify the team when metrics degrade.
// Alerting service
async function checkAlerts() {
const metrics = ['LCP', 'FID', 'CLS', 'TTFB'];
const thresholds = {
LCP: { poor: 4000 }, // Alert if p75 LCP > 4s
FID: { poor: 300 },
CLS: { poor: 0.25 },
TTFB: { poor: 1500 }
};
for (const metric of metrics) {
const data = await fetch(`http://localhost:3001/api/rum/aggregate/${metric}`).then(r => r.json());
if (data.p75 > thresholds[metric].poor) {
console.error(`ALERT: ${metric} p75 (${data.p75}) exceeds poor threshold (${thresholds[metric].poor})`);
// Send alert to PagerDuty, Slack, or email
}
}
}
setInterval(checkAlerts, 300000); // Check every 5 minutes
Step 6: Segment Data by Dimension
Break down RUM data by device type, connection speed, geography, and page to identify specific problem areas.
function sendToAnalytics(metric) {
const body = {
...metric,
page: window.location.pathname,
device: navigator.userAgentData?.mobile ? 'mobile' : 'desktop',
connection: navigator.connection?.effectiveType || 'unknown',
country: 'unknown', // Set via server-side geo-IP lookup
experiment: document.cookie.includes('experiment=A') ? 'A' : 'control'
};
navigator.sendBeacon('/api/rum', JSON.stringify(body));
}
Step 7: Use CrUX API for Free Field Data
The Chrome User Experience Report (CrUX) provides field data for any public URL. Use it to validate your own RUM data.
# Query CrUX API for a URL
curl -X POST "https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=$API_KEY" \
-H "Content-Type: application/json" \
--data '{"url": "https://dodatech.com", "formFactor": "PHONE"}'
Expected output: JSON data showing the distribution of LCP, FID, CLS across good/needs-improvement/poor for real Chrome users.
Step 8: Correlate RUM with Business Metrics
Track how performance changes affect conversion rates, bounce rates, and revenue.
async function correlatePerformance() {
const rum = await fetch('/api/rum/aggregate/LCP').then(r => r.json());
const revenue = await fetch('/api/revenue/daily').then(r => r.json());
// Simple correlation: plot LCP p75 against daily revenue
console.log(`LCP p75: ${rum.p75}ms, Revenue: $${revenue.today}`);
// Expected: As LCP increases, revenue decreases
}
Learning Path
flowchart LR A[Performance Testing] --> B[Real User Monitoring] B --> C[Performance Budgets] B --> D[Third-Party Script Impact] C --> E[Continuous Improvement] style B fill:#4f46e5,color:#fff style A fill:#6366f1,color:#fff style C fill:#6366f1,color:#fff
Common Errors
Sending RUM data synchronously: Synchronous XHR or fetch during page unload may be cancelled. Always use
navigator.sendBeacon()for RUM data transmission.Collecting metrics on every interaction: Only collect metrics on initial page load and significant user interactions. Sampling 1-10 percent of visitors is often sufficient and reduces noise.
Not filtering bot traffic: Automated bots and crawlers generate misleading RUM data. Filter by
navigator.webdriverand user-agent patterns.Ignoring the cross-origin isolation issue: Some performance APIs require
Cross-Origin-Embedder-PolicyandCross-Origin-Opener-Policyheaders to provide precise timing data.Storing RUM data without aggregation: Raw RUM data grows quickly. Aggregate data by the minute or hour and store aggregates for long-term trending.
Sampling inconsistently: If you sample 10 percent of users, ensure you sample consistently. Varying sample rates between page types skews comparisons.
Practice Questions
- What is the difference between RUM field data and Lighthouse lab data?
- Why is navigator.sendBeacon preferred for RUM data transmission?
- What is the CrUX API and how does it complement RUM?
- Why should you segment RUM data by device and connection type?
- How can you correlate performance data with business metrics?
Answers: 1. Lab data is collected in a controlled environment; field data comes from real users under real-world conditions including actual devices, networks, and locations. 2. sendBeacon guarantees delivery even during page unload, unlike fetch or XHR which may be cancelled. 3. The Chrome User Experience Report provides free field data from real Chrome users, validating your own RUM collection. 4. Performance varies significantly between mobile and desktop, and between 4G and 3G connections. Segmentation identifies specific problem cohorts. 5. By time-correlating performance metrics with conversion rates, bounce rates, or revenue data, you can quantify the business impact of performance.
Challenge
Implement a complete RUM pipeline for a sample site: add the web-vitals library, build a server-side ingestion endpoint, store the data in memory with hourly aggregation, create a dashboard showing p75 LCP and good/needs-improvement/poor distribution, and set up an alert that fires when the poor percentage exceeds 10 percent.
FAQ
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro