Web Vitals messen, senden und auswerten: direkt aus der Produktion
Synthetische Tests zeigen nur eine simulierte Momentaufnahme, doch echte Kunden erleben Magento und Hyvä Shops auf unterschiedlichsten Geräten, Netzwerken und Verbindungsqualitäten. Dieser Artikel zeigt technisch fundiert, wie man die web-vitals Bibliothek integriert, Messwerte effizient per sendBeacon überträgt, bei hohem Traffic sinnvoll sampelt, ein minimales Ingestion Backend baut und dabei durchgehend datenschutzkonform bleibt.
Inhaltsverzeichnis
- 1. RUM und synthetisches Monitoring: unterschiedliche Fragen, unterschiedliche Antworten
- 2. Die web-vitals-Bibliothek einbinden: LCP, INP, CLS, TTFB und FCP erfassen
- 3. Beacon-Versand mit navigator.sendBeacon statt fetch
- 4. Sampling-Strategie: Warum 100 Prozent Erfassung bei Traffic-starken Shops scheitert
- 5. Stratifiziertes Sampling nach Seitentyp
- 6. Ein minimaler Ingestion-Endpoint für RUM-Daten
- 7. Aggregation in Perzentile: p50, p75 und p95
- 8. Datenschutz: PII-freie Beacons, IP-Anonymisierung und DSGVO
- 9. RUM vs. Synthetic Monitoring im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. RUM und synthetisches Monitoring: unterschiedliche Fragen, unterschiedliche Antworten
Synthetic Monitoring, etwa über Lighthouse CI, WebPageTest oder geplante Puppeteer-Läufe, misst Performance in einer kontrollierten, reproduzierbaren Umgebung mit fester Netzwerkgeschwindigkeit, festem Gerät und ohne Nutzervariabilität. Das macht Regressionen zwischen zwei Deployments exakt vergleichbar, weil praktisch alle Störvariablen ausgeschlossen sind. Real User Monitoring (RUM) dagegen misst genau die Ereignisse, die echte Besucher in echten Browsern auf echten Geräten und Netzwerken erleben, inklusive langsamer Mobilfunkverbindungen, überlasteter Low-End-Smartphones und Tabs im Hintergrund, die synthetische Tests niemals abbilden.
Der Unterschied zeigt sich am deutlichsten bei LCP und INP: Ein synthetischer Test auf einer Referenzmaschine mit stabiler Leitung kann grüne Werte liefern, während der reale p75-Wert unter echten Nutzern durch schwaches Mobilfunknetz oder ältere Android-Geräte im roten Bereich liegt. Wer nur synthetisch misst, optimiert im schlimmsten Fall für ein Szenario, das kaum ein Kunde tatsächlich erlebt. RUM schließt diese Lücke, kann aber selbst keine kontrollierten Vorab-Vergleiche vor dem Deployment liefern, weshalb Synthetic Monitoring in der CI/CD-Pipeline unverzichtbar bleibt.
Die robusteste Strategie kombiniert beide Ansätze: Synthetic Monitoring als Frühwarnsystem direkt im Deployment-Prozess, RUM als kontinuierliches Signal aus der Produktion, das die tatsächliche Nutzererfahrung über Gerätetypen, Netzwerke und Regionen hinweg abbildet. Die folgenden Abschnitte konzentrieren sich auf den technischen Aufbau der RUM-Seite dieser Kombination.
2. Die web-vitals-Bibliothek einbinden: LCP, INP, CLS, TTFB und FCP erfassen
Die von Google gepflegte web-vitals-Bibliothek kapselt die komplizierten Browser-APIs (PerformanceObserver, Layout Instability API, Event Timing API) hinter fünf einfachen Funktionen: onLCP, onINP, onCLS, onTTFB und onFCP. Jede Funktion registriert einen Callback, der ein Metric-Objekt mit name, value, rating (good, needs-improvement, poor), id und delta liefert, sobald der Browser den finalen Wert ermitteln kann. Bei CLS und INP passiert das oft erst beim Verlassen der Seite, weil beide Metriken über die gesamte Sitzungsdauer akkumulieren.
Wichtig ist der Unterschied zwischen den Reporting-Modi: Im Standardmodus feuert der Callback nur einmal mit dem finalen Wert, im reportAllChanges-Modus dagegen bei jeder Wertänderung, was für lokales Debugging nützlich ist, aber in Produktion unnötigen Traffic erzeugt. Für ein RUM-Setup registriert man alle fünf Metriken direkt beim Seitenaufbau in einem schlanken Bootstrap-Skript und übergibt jeden Wert sofort an die Versandfunktion, sobald der jeweilige Callback feuert, statt am Seitenende zu warten, weil TTFB und FCP oft schon Sekunden vor LCP und INP feststehen.
import { onLCP, onINP, onCLS, onTTFB, onFCP } from 'web-vitals';
// Collect a metric and forward it as soon as it becomes final
function reportMetric(metric) {
const payload = {
name: metric.name,
value: metric.value,
rating: metric.rating,
id: metric.id,
navigationType: metric.navigationType,
page: location.pathname,
ts: Date.now()
};
sendRumBeacon(payload);
}
onLCP(reportMetric);
onINP(reportMetric);
onCLS(reportMetric);
onTTFB(reportMetric);
onFCP(reportMetric);
3. Beacon-Versand mit navigator.sendBeacon statt fetch
navigator.sendBeacon() wurde exakt für dieses Problem entworfen: Metriken wie CLS und INP stehen oft erst fest, wenn der Nutzer die Seite bereits verlässt, im visibilitychange- oder pagehide-Event. Ein normaler fetch()- oder XHR-Aufruf wird an dieser Stelle vom Browser häufig abgebrochen, sobald der Rendering-Prozess terminiert wird, weil der Request nicht garantiert im Hintergrund weiterläuft. sendBeacon() übergibt die Daten dagegen asynchron an den Browser-Prozess, der die Zustellung unabhängig vom Lebenszyklus der Seite garantiert, und liefert synchron ein Boolean zurück, ob der Request angenommen wurde.
Die Kehrseite: sendBeacon() unterstützt nur POST, kein benutzerdefiniertes Header-Setzen außer dem Content-Type über den Blob-MIME-Type, und ist auf ein Payload-Limit von typischerweise 64 KB pro Origin über alle ausstehenden Beacons begrenzt. Für ältere Browser oder Umgebungen ohne sendBeacon-Support ist fetch() mit keepalive: true der korrekte Fallback: keepalive signalisiert dem Browser dasselbe Verhalten, funktioniert aber nicht überall gleich zuverlässig, weshalb sendBeacon() wo verfügbar immer die erste Wahl bleibt.
// Send the RUM payload reliably, even during page unload
function sendRumBeacon(payload) {
const url = 'https://rum.mironsoft.de/collect';
const body = JSON.stringify(payload);
if (navigator.sendBeacon) {
const blob = new Blob([body], { type: 'application/json' });
const queued = navigator.sendBeacon(url, blob);
if (queued) return;
}
// Fallback for browsers without sendBeacon or a rejected queue
fetch(url, {
method: 'POST',
body,
headers: { 'Content-Type': 'application/json' },
keepalive: true
}).catch(() => {
// Silently drop the event, RUM must never break the page
});
}
4. Sampling-Strategie: Warum 100 Prozent Erfassung bei Traffic-starken Shops scheitert
Ein Magento-Shop mit 500.000 Sessions im Monat und fünf Metriken pro Pageload erzeugt bei voller Erfassung mehrere Millionen Events monatlich. Ingestion-Server, Datenbank und Speicherkosten skalieren linear mit, während der statistische Erkenntnisgewinn oberhalb einer bestimmten Stichprobengröße kaum noch steigt. Perzentile wie p75 stabilisieren sich bereits bei wenigen tausend Samples pro Seitentyp und Zeitfenster, jedes zusätzliche Event darüber hinaus kostet primär Infrastruktur, ohne die statistische Aussagekraft nennenswert zu verbessern.
Die gängige Lösung ist eine clientseitige Sampling-Entscheidung direkt beim Seitenaufbau, bevor überhaupt Metriken gesammelt werden: Ein Zufallswert wird gegen eine konfigurierte Sample-Rate geprüft, und nur bei Erfolg wird die web-vitals-Bibliothek überhaupt initialisiert. Das spart nicht nur Netzwerk-Traffic, sondern auch CPU-Zeit auf dem Client, weil PerformanceObserver-Instanzen für nicht gesampelte Sessions gar nicht erst erstellt werden. Übliche Sample-Raten liegen zwischen 5 % bei sehr hohem Traffic und 100 % bei kleinen Shops unter etwa 10.000 Sessions im Monat.
// Decide once per session whether this visitor is sampled
function isSampled(sampleRate) {
const key = 'rum_sampled';
let decision = sessionStorage.getItem(key);
if (decision === null) {
decision = Math.random() < sampleRate ? '1' : '0';
sessionStorage.setItem(key, decision);
}
return decision === '1';
}
// Only bootstrap web-vitals collection for sampled sessions
if (isSampled(0.1)) {
initWebVitalsCollection();
}
5. Stratifiziertes Sampling nach Seitentyp
Eine einheitliche Sample-Rate über den gesamten Shop hinweg ignoriert, dass nicht alle Seiten gleich wichtig sind. Die Checkout-Seite hat typischerweise nur einen Bruchteil des Traffics der Kategorieseiten, aber jede Performance-Regression dort kostet direkt Umsatz. Eine niedrige Sample-Rate liefert hier zu wenige Datenpunkte für belastbare p75-Werte. Kategorie- und Produktseiten dagegen erzeugen genug Traffic, dass auch eine niedrige Sample-Rate von 5 bis 10 Prozent ausreichend Signal liefert.
Die praktische Umsetzung ist eine Lookup-Tabelle im Bootstrap-Skript, die anhand eines vom Server injizierten page_type-Attributs, etwa via data-page-type am body-Tag, die passende Sample-Rate wählt: 100 % für Checkout und Zahlungsseiten, 25 % für Produktdetailseiten, 5 % für Kategorie- und CMS-Seiten. So bleibt das Gesamtvolumen kontrollierbar, während die geschäftskritischen Flows vollständige statistische Abdeckung behalten. Diese Zuordnung sollte serverseitig konfigurierbar sein, nicht hartkodiert im JavaScript-Bundle, damit sie ohne Deployment angepasst werden kann.
6. Ein minimaler Ingestion-Endpoint für RUM-Daten
Ein RUM-Backend muss zunächst nur eine Aufgabe zuverlässig erfüllen: eingehende Beacons validieren, vor offensichtlichem Missbrauch schützen und performant persistieren, ohne den Request-Thread mit teurer Verarbeitung zu blockieren. Der Endpoint sollte ausschließlich POST mit JSON-Body akzeptieren, den name gegen eine Whitelist (LCP, INP, CLS, TTFB, FCP) prüfen, die Payload-Größe hart begrenzen und offensichtlich unplausible Werte, etwa einen negativen LCP-Wert oder ein CLS über 10, verwerfen, statt sie ungeprüft in die Datenbank zu schreiben.
Für die Antwort genügt ein knapper 204 No Content ohne Body, der Browser wertet die Antwort eines sendBeacon-Requests ohnehin nicht aus, jede zusätzliche Antwortlast ist reine Verschwendung. In der Praxis lohnt es sich außerdem, Schreiboperationen zu bündeln statt pro Event einen einzelnen INSERT abzusetzen: entweder über eine Message-Queue, etwa eine Redis-Liste als Puffer, oder durch Batch-Inserts, die alle paar Sekunden gesammelte Events in einem Rutsch schreiben.
<?php
declare(strict_types=1);
// Minimal RUM ingestion endpoint, no framework dependency
header('Content-Type: text/plain');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit;
}
$raw = file_get_contents('php://input', false, null, 0, 8192);
$payload = json_decode($raw ?: '', true);
$allowedMetrics = ['LCP', 'INP', 'CLS', 'TTFB', 'FCP'];
if (
!is_array($payload)
|| !in_array($payload['name'] ?? '', $allowedMetrics, true)
|| !is_numeric($payload['value'] ?? null)
|| (float) $payload['value'] < 0
|| (float) $payload['value'] > 60000
) {
http_response_code(422);
exit;
}
// Anonymize the visitor IP before it ever reaches storage
$ipParts = explode('.', $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0');
$ipParts[3] = '0';
$anonymizedIp = implode('.', $ipParts);
$pdo = new PDO('mysql:host=127.0.0.1;dbname=rum', 'rum_writer', getenv('RUM_DB_PASSWORD'));
$stmt = $pdo->prepare(
'INSERT INTO rum_events (metric_name, value, rating, page_type, page_path, ip_hash, created_at)
VALUES (:name, :value, :rating, :page_type, :page_path, :ip_hash, NOW())'
);
$stmt->execute([
'name' => $payload['name'],
'value' => (float) $payload['value'],
'rating' => $payload['rating'] ?? 'unknown',
'page_type' => substr((string) ($payload['page_type'] ?? 'unknown'), 0, 32),
'page_path' => substr((string) ($payload['page'] ?? '/'), 0, 255),
'ip_hash' => hash('sha256', $anonymizedIp . getenv('RUM_SALT')),
]);
http_response_code(204);
7. Aggregation in Perzentile: p50, p75 und p95
Rohereignisse in einer Zeitreihe sind für ein Dashboard nutzlos, solange sie nicht zu Perzentilen aggregiert werden. Der Mittelwert ist für Performance-Metriken irreführend, weil einzelne Ausreißer, etwa ein Nutzer mit sehr langsamer Verbindung, den Durchschnitt verzerren, ohne die typische Erfahrung widerzuspiegeln. p75 hat sich als Standard etabliert, weil er drei Viertel aller Sessions abdeckt und damit robuster gegen einzelne Extremwerte ist als p50, aber empfindlicher für echte Verschlechterungen als p95.
MySQL unterstützt ab Version 8.0 PERCENTILE_CONT als Window-Funktion direkt in SQL, was eine separate Aggregations-Pipeline für einfache Dashboards unnötig macht. Für größere Datenmengen lohnt sich eine stündliche Vor-Aggregation in eine separate Tabelle, aus der das Dashboard liest, statt bei jeder Abfrage über Millionen Rohzeilen zu scannen. Die Rohtabelle kann danach nach 30 bis 90 Tagen bereinigt werden, während die aggregierten Perzentile dauerhaft erhalten bleiben.
-- Aggregate p50/p75/p95 per metric and page type for the last 24 hours
SELECT DISTINCT
metric_name,
page_type,
COUNT(*) OVER (PARTITION BY metric_name, page_type) AS sample_count,
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY value)
OVER (PARTITION BY metric_name, page_type) AS p50,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY value)
OVER (PARTITION BY metric_name, page_type) AS p75,
PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY value)
OVER (PARTITION BY metric_name, page_type) AS p95
FROM rum_events
WHERE created_at >= NOW() - INTERVAL 24 HOUR;
8. Datenschutz: PII-freie Beacons, IP-Anonymisierung und DSGVO
Ein RUM-Beacon darf grundsätzlich keine personenbezogenen Daten enthalten: keine E-Mail-Adressen, keine Kundennummern, keine vollständige URL mit Query-Parametern, die Suchbegriffe oder Formulareingaben enthalten könnten. Der Payload beschränkt sich auf Metrikname, Wert, Rating, einen groben Seitentyp und den Pfad ohne Query-String, das reicht für jede sinnvolle Performance-Analyse aus und minimiert gleichzeitig die Angriffsfläche für Datenschutzverstöße.
Die IP-Adresse des Besuchers gilt in der EU als personenbezogenes Datum und darf nicht dauerhaft im Klartext gespeichert werden. Gängige Praxis ist entweder das Kürzen des letzten Oktetts, wie im Ingestion-Beispiel oben, oder das Hashen mit einem täglich rotierenden Salt, sodass keine Rückverfolgung über Tage hinweg möglich ist, aber Missbrauchserkennung weiterhin funktioniert. Weil ein so konfiguriertes RUM-Setup keine Cookies setzt und keine geräteübergreifende Wiedererkennung ermöglicht, lässt es sich in vielen Fällen auf Basis berechtigten Interesses nach Art. 6 Abs. 1 lit. f DSGVO betreiben, ohne Cookie-Consent-Banner einzublenden, im Unterschied zu Drittanbieter-Analytics-Tools, die typischerweise persistente Client-IDs und cross-site Cookies zur Nutzerwiedererkennung setzen und damit eine Einwilligung erfordern.
Diese Einschätzung ersetzt keine Rechtsberatung im Einzelfall, aber die technische Grundregel bleibt robust: Je weniger Datenpunkte pro Event gespeichert werden und je kürzer die Aufbewahrungsfrist der Rohdaten ist, desto geringer das Compliance-Risiko und desto einfacher lässt sich das Setup datenschutzkonform betreiben.
9. RUM vs. Synthetic Monitoring im direkten Vergleich
Die folgende Übersicht fasst zusammen, wo RUM und synthetisches Monitoring jeweils ihre Stärken ausspielen und wo die jeweils andere Methode die Lücke schließen muss.
| Dimension | RUM | Synthetic Monitoring | Einordnung |
|---|---|---|---|
| Datenquelle | Reale Nutzer-Sessions | Simulierte Bot-Läufe | Kombination nötig für vollständiges Bild |
| Abdeckung | Nur tatsächlich besuchte Seiten | Jede konfigurierte Seite, auch vor Launch | Synthetic deckt RUM-Lücken vor Go-Live ab |
| Kosten bei hohem Traffic | Steigt mit Traffic, Sampling nötig | Fixkosten pro Check, unabhängig vom Traffic | RUM erfordert Sampling-Strategie ab Skalierung |
| Reale Geräte-/Netzwerkprobleme | Erfasst echte Ausreißer zuverlässig | Simulierte Bedingungen, verpasst Gerätevielfalt | RUM einzige Quelle für echte Nutzerrealität |
| Regressions-Erkennung | Braucht Sample-Volumen, verzögertes Signal | Sofortiger Alarm nach jedem Deployment | Synthetic für schnelles CI/CD-Feedback |
In der Praxis ergänzen sich beide Ansätze, statt sich gegenseitig zu ersetzen: Synthetic Monitoring fängt Regressionen sofort im Deployment-Prozess ab, RUM zeigt, wie sich diese Regressionen tatsächlich auf echte Kunden über alle Geräte und Netzwerke hinweg auswirken. Ein Dashboard, das beide Datenquellen nebeneinander mit derselben Metrik-Definition darstellt, macht Abweichungen zwischen Labor- und Feld-Daten sofort sichtbar.
Mironsoft
RUM-Setup, Web-Vitals-Monitoring und Hyvä-Performance für Magento-Shops
Real User Monitoring professionell aufsetzen?
Wir bauen euer RUM-Setup von der web-vitals-Integration über die Sampling-Strategie bis zum Ingestion-Backend, mit Perzentil-Dashboard und einer Konfiguration, die datenschutzkonform ohne unnötige Consent-Hürden auskommt.
RUM-Implementierung
web-vitals-Integration, sendBeacon-Versand und Sampling passend zu eurem Traffic
Dashboard-Setup
Ingestion-Backend, Perzentil-Aggregation und Regressions-Alerts
Privacy-konforme Konfiguration
IP-Anonymisierung, PII-freie Payloads und DSGVO-konforme Aufbewahrungsfristen
10. Zusammenfassung
Real User Monitoring löst ein Problem, das synthetische Tests strukturell nicht lösen können: zu zeigen, wie Magento- und Hyvä-Shops tatsächlich bei echten Kunden auf echten Geräten und Netzwerken performen. Die web-vitals-Bibliothek liefert die Messwerte für LCP, INP, CLS, TTFB und FCP direkt aus dem Browser, navigator.sendBeacon() stellt sicher, dass diese Werte auch beim Verlassen der Seite noch zuverlässig ankommen, mit fetch(..., { keepalive: true }) als Fallback für Ausnahmefälle.
Ab einer gewissen Traffic-Größe ist eine durchdachte, nach Seitentyp stratifizierte Sampling-Strategie kein Nice-to-have, sondern Voraussetzung dafür, dass Ingestion-Backend und Speicherkosten beherrschbar bleiben. Ein minimaler Ingestion-Endpoint mit Whitelist-Validierung, Batch-Inserts und Perzentil-Aggregation auf p50, p75 und p95 reicht für ein aussagekräftiges Dashboard aus, solange konsequent auf PII-freie Payloads und IP-Anonymisierung geachtet wird, was ein solches Setup häufig sogar ohne Cookie-Consent-Banner betreibbar macht.
Real User Monitoring technisch aufsetzen - Das Wichtigste auf einen Blick
web-vitals + sendBeacon
LCP, INP, CLS, TTFB und FCP per web-vitals erfassen und zuverlässig per sendBeacon() übertragen, mit fetch keepalive als Fallback.
Sampling-Strategie
Sample-Rate an das Traffic-Volumen anpassen (5-100 %), stratifiziert nach Seitentyp: Checkout höher gewichten als Kategorieseiten.
Minimales Backend
Schlanker Ingestion-Endpoint mit Whitelist-Validierung, Batch-Inserts und Perzentil-Aggregation (p50/p75/p95) statt Rohdaten-Dashboards.
Datenschutz
PII-freie Beacons, IP-Anonymisierung und in der Regel keine Einwilligung nötig, anders als bei cookie-basierten Drittanbieter-Analytics.