TL;DR: Labortests (Lighthouse, CI-Benchmarks) zeigen, wie Ihre App auf einer Testmaschine funktioniert. Real User Monitoring zeigt, wie Ihre App für echte Nutzer auf deren Geräten, Netzwerken und Standorten funktioniert. RUM erkennt Performance-Probleme, die Labortests niemals finden werden – langsame Verbindungen, Speicherdruck, Ad-Blocker-Interferenz und geografische Varianz.
Labordaten vs. Felddaten
| Aspekt | Labor (Lighthouse) | Feld (RUM) |
|---|---|---|
| Umgebung | Kontrolliert (Moto G4, langsames 3G) | Echte Nutzergeräte |
| Netzwerk | Simulierte Drosselung | Tatsächliche Verbindungen (5G, 4G, 3G, WiFi) |
| Standort | Einzelner Standort | Global (330+ Cloudflare-Standorte) |
| Gerät | Festes Geräteprofil | Alle Geräte und Formfaktoren |
| Stichprobengröße | Einzelner Durchlauf pro PR | Jeder Seitenaufruf |
| Erkennt | Optimierungspotenziale | Tatsächliche Nutzererfahrungs-Probleme |
| Fehlt | Das, was echte Nutzer erleben | Kontrollierter Vergleich |
Die Wahrheit: Labordaten zeigen, was Sie reparieren sollten. Felddaten zeigen, was Nutzer tatsächlich erleben. Sie brauchen beides.
RUM-Datenerfassung
Web Vitals-Erfassung einrichten
Web Vitals von echten Nutzern zu erfassen ist der erste Schritt jeder RUM-Implementierung. Die web-vitals-Bibliothek liefert attribut-bewusste Metriken, die nicht nur den Wert angeben, sondern auch die Ursache:
// src/lib/rum.ts
import { onLCP, onCLS, onINP, onTTFB, onFCP } from "web-vitals/attribution"
type VitalName = "LCP" | "CLS" | "INP" | "TTFB" | "FCP"
interface VitalReport {
name: VitalName
value: number
rating: "good" | "needs-improvement" | "poor"
id: string
metadata: Record<string, string>
deviceType: string
connectionType: string
}
export function initRUM() {
const vitals: Array<{ name: VitalName; fn: (metric: any) => void }> = [
{ name: "LCP", fn: onLCP },
{ name: "CLS", fn: onCLS },
{ name: "INP", fn: onINP },
{ name: "TTFB", fn: onTTFB },
{ name: "FCP", fn: onFCP },
]
vitals.forEach(({ name, fn }) => {
fn((metric) => {
sendVital({
name,
value: metric.value,
rating: metric.rating,
id: metric.id,
metadata: extractAttribution(metric),
deviceType: getDeviceType(),
connectionType: getConnectionType(),
})
})
})
}
function extractAttribution(metric: any): Record<string, string> {
if (metric.attribution) {
// Nützliche Debugging-Infos extrahieren
const { element, url, fcp, ...rest } = metric.attribution
return {
...(element && { lcpElement: element.tagName }),
...(url && { lcpUrl: url }),
}
}
return {}
}
function getDeviceType(): string {
const ua = navigator.userAgent
if (/Mobi|Android/i.test(ua)) return "mobile"
if (/Tablet|iPad/i.test(ua)) return "tablet"
return "desktop"
}
function getConnectionType(): string {
const conn = (navigator as any).connection
return conn?.effectiveType ?? "unknown"
}
RUM-Daten an das Backend senden
Sobald die Daten erfasst sind, müssen die Vitalmetriken zuverlässig an Ihren Server übermittelt werden. Mit navigator.sendBeacon werden Daten auch gesendet, wenn der Nutzer die Seite verlässt:
// sendBeacon für zuverlässige Übermittlung verwenden (überlebt Seitennavigation)
function sendVital(report: VitalReport) {
const payload = {
...report,
pathname: window.location.pathname,
timestamp: Date.now(),
}
if (navigator.sendBeacon) {
navigator.sendBeacon("/api/vitals", JSON.stringify(payload))
} else {
fetch("/api/vitals", {
method: "POST",
body: JSON.stringify(payload),
keepalive: true,
})
}
}
Serverseitige Speicherung
Auf der Serverseite wird jeder Metrikbericht mit Kontextmetadaten wie Land und Gerätetyp in D1 gespeichert, um ihn später analysieren zu können:
// server/rum.ts
export const reportVital = createServerFn({ method: "POST" }).handler(
async ({ request, context }) => {
const data = await request.json()
// In D1 für Abfragen speichern
await context.env.DB.prepare(`
INSERT INTO rum_metrics (
id, name, value, rating, pathname,
device_type, connection_type,
country, metadata, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).bind(
data.id,
data.name,
data.value,
data.rating,
data.pathname,
data.deviceType,
data.connectionType,
request.cf?.country ?? "unknown",
JSON.stringify(data.metadata),
data.timestamp
).run()
return { received: true }
}
)
RUM-Daten analysieren
Nach Metrik abfragen
Nachdem genügend Daten gesammelt wurden, fragen Sie diese ab, um Ihre Performance-Basislinie zu verstehen. Diese Dashboard-Funktion berechnet p50-, p75- und p95-Perzentile sowie den Anteil der „guten" Bewertungen für jede Metrik:
export const getRumDashboard = createServerFn({ method: "GET" }).handler(
async ({}, { context }) => {
// Gesamtmetriken für die letzten 7 Tage
const overall = await context.env.DB.prepare(`
SELECT
name,
COUNT(*) as samples,
APPROX_PERCENTILE(value, 0.5) as p50,
APPROX_PERCENTILE(value, 0.75) as p75,
APPROX_PERCENTILE(value, 0.95) as p95,
SUM(CASE WHEN rating = 'good' THEN 1 ELSE 0 END) * 100.0 / COUNT(*) as good_pct
FROM rum_metrics
WHERE created_at > datetime('now', '-7 days')
GROUP BY name
`).all()
// Aufschlüsselung nach Pfad (Top 20 langsamste)
const byPath = await context.env.DB.prepare(`
SELECT
pathname,
APPROX_PERCENTILE(CASE WHEN name = 'LCP' THEN value END, 0.5) as lcp_p50,
APPROX_PERCENTILE(CASE WHEN name = 'INP' THEN value END, 0.5) as inp_p50,
COUNT(*) as pageviews
FROM rum_metrics
WHERE created_at > datetime('now', '-7 days')
GROUP BY pathname
ORDER BY lcp_p50 DESC
LIMIT 20
`).all()
return { overall: overall.results, slowestPaths: byPath.results }
}
)
RUM-Dashboard
RUM Dashboard (Letzte 7 Tage)
Web Vitals Übersicht:
┌────────┬──────────┬──────────┬──────────┬─────────┐
│ Metrik │ P50 │ P75 │ P95 │ % Gut │
├────────┼──────────┼──────────┼──────────┼─────────┤
│ LCP │ 1.200 ms │ 2.100 ms │ 4.500 ms │ 78 % │
│ CLS │ 0,02 │ 0,05 │ 0,15 │ 85 % │
│ INP │ 80 ms │ 150 ms │ 350 ms │ 82 % │
│ TTFB │ 150 ms │ 350 ms │ 900 ms │ 88 % │
└────────┴──────────┴──────────┴──────────┴─────────┘
Performance nach geografischer Region:
┌───────────────┬──────────┬──────────┬───────────────┐
│ Region │ P50 LCP │ P95 LCP │ Langsam % (>3s) │
├───────────────┼──────────┼──────────┼───────────────┤
│ US East │ 900 ms │ 2.100 ms │ 3 % │
│ US West │ 1.100 ms │ 2.800 ms │ 5 % │
│ Europa │ 1.300 ms │ 3.200 ms │ 8 % │
│ Asien-Pazifik │ 2.100 ms │ 5.500 ms │ 20 % │
│ Südamerika │ 2.400 ms │ 6.000 ms │ 25 % │
└───────────────┴──────────┴──────────┴───────────────┘
Top 5 langsamste Seiten:
1. /dashboard/analytics (p50 LCP: 4,2 s) — schwere Diagramme
2. /products/listing (p50 LCP: 3,8 s) — große Bilder
3. /reports/export (p50 LCP: 3,5 s) — langsame API
Alerts aus RUM-Daten
Automatisierte Alerts verwandeln rohe RUM-Daten in umsetzbare Benachrichtigungen. Diese Funktion prüft die LCP-Qualität und identifiziert durchgehend langsame Seiten:
export const checkRumAlerts = createServerFn({ method: "GET" }).handler(
async ({}, { context }) => {
const alerts = []
// Alert, wenn Anteil guter LCP unter den Schwellenwert fällt
const lcpQuality = await context.env.DB.prepare(`
SELECT
COUNT(*) as total,
SUM(CASE WHEN rating = 'good' THEN 1 ELSE 0 END) * 100.0 / COUNT(*) as good_pct
FROM rum_metrics
WHERE name = 'LCP' AND created_at > datetime('now', '-1 hour')
`).first()
if (lcpQuality && Number(lcpQuality.good_pct) < 70) {
alerts.push({
type: "rum_degradation",
metric: "LCP",
goodPct: Math.round(Number(lcpQuality.good_pct)),
threshold: 70,
severity: "high",
})
}
// Alert, wenn eine bestimmte Seite p95 LCP > 5 s hat
const slowPages = await context.env.DB.prepare(`
SELECT pathname, COUNT(*) as views
FROM rum_metrics
WHERE name = 'LCP'
AND value > 5000
AND created_at > datetime('now', '-1 hour')
GROUP BY pathname
HAVING views > 10
ORDER BY views DESC
LIMIT 5
`).all()
if (slowPages.results.length > 0) {
alerts.push({
type: "slow_pages",
pages: slowPages.results,
severity: "medium",
})
}
return alerts
}
)
RUM zur Steuerung von Optimierungen nutzen
| RUM-Signal | Untersuchung | Optimierung |
|---|---|---|
| Hohes LCP auf Mobilgeräten | Hero-Bildgröße prüfen | AVIF ausliefern, Hero vorladen, Bildgröße reduzieren |
| Hohes CLS auf Produktseite | Dynamische Inhaltseinfügungen prüfen | Platz reservieren, Font-Swap-Layoutverschiebung beheben |
| Hohes INP im Dashboard | Hauptthread-Aktivität profilieren | Lange Tasks aufteilen, Diagramme verzögert laden |
| Schlechtes APAC LCP | Geografisches Latenzproblem | Edge Caching, CDN-Optimierung |
| Schlechtes TTFB auf Auth-Seiten | Auth-Middleware-Overhead | Session-Lookup optimieren, Auth-Status zwischenspeichern |
RUM-Datenschema
-- migrations/rum_metrics.sql
CREATE TABLE IF NOT EXISTS rum_metrics (
id TEXT PRIMARY KEY,
name TEXT NOT NULL, -- LCP, CLS, INP, TTFB, FCP
value REAL NOT NULL, -- Metrikwert in ms oder Score
rating TEXT NOT NULL, -- good, needs-improvement, poor
pathname TEXT NOT NULL, -- URL-Pfad
device_type TEXT, -- mobile, desktop, tablet
connection_type TEXT, -- 4g, 3g, 2g, slow-2g
country TEXT, -- Zweistelliger Ländercode
metadata TEXT, -- JSON mit Attributionsdaten
created_at INTEGER NOT NULL
);
CREATE INDEX idx_rum_name ON rum_metrics(name);
CREATE INDEX idx_rum_created ON rum_metrics(created_at);
CREATE INDEX idx_rum_path ON rum_metrics(pathname);
RUM-Implementierungs-Checkliste
- Web Vitals-Bibliothek auf allen Seiten installiert und initialisiert
- RUM-Daten per sendBeacon für zuverlässige Übermittlung gesendet
- Server-Endpunkt speichert Metriken in D1 oder Analytics Engine
- Sample-Rate konfiguriert (100 % für Erstaufbau, dann auf 10–25 % reduzieren)
- Dashboard für p50/p75/p95-Metriken aufgebaut
- Geografische Aufschlüsselung im Dashboard sichtbar
- Pfadbasierte Aggregation zur Erkennung langsamer Seiten
- Automatisierte Alerts für RUM-Degradation
- Gerätetyp-Segmentierung (Mobilgeräte vs. Desktop)
- Verbindungstyp-Tracking für netzwerk-bewusste Optimierung
- Integration in CI-Pipeline — PR RUM mit Produktions-RUM vergleichen
- Historische Datenaufbewahrung für Trendanalyse
Fazit
Real User Monitoring schließt die Lücke zwischen dem, was Sie in der Entwicklung testen, und dem, was Ihre Nutzer in der Produktion erleben. Ohne RUM optimieren Sie auf Basis von Annahmen. Mit RUM basiert jede Optimierungsentscheidung auf Daten echter Nutzer.
Die Implementierung ist straightforward:
- Web Vitals bei jedem Seitenaufruf erfassen
- In D1 oder Analytics Engine speichern
- Dashboards zur Visualisierung aufbauen
- Alerts für Degradation einrichten
- Daten zur Priorisierung der Optimierungsarbeit nutzen
Um Ihre Performance-Praxis zu vertiefen, lesen Sie unseren Core Web Vitals Optimization Guide für Metrik-Verbesserungsstrategien, Performance Budgeting zur Vermeidung von Regressionen, SaaS Monitoring and Observability für die Infrastruktur-Gesundheit und Lighthouse 95+ Optimization Tips für Labor-basierte Audits.