Der Übergang des Sichtbarkeitszustands: Seitenaufrufe ohne verlässliche Beacons messen
pagehide, visibilitychange und die Beacon API verhalten sich in jedem Browser anders. So erfassen Sie das Ende einer Sitzung zuverlässig — ohne Cookies und ohne persistente Identifikatoren.
Zu erfassen, dass eine Seite aufgerufen wurde, ist leicht. Zu erfassen, wann Nutzerinnen und Nutzer tatsächlich gegangen sind — zuverlässig, über Tabwechsel, Hintergrundwechsel und harte Schließvorgänge hinweg — gehört zu den kniffligeren Problemen der Web-Analytik. Das ist wichtig, weil Engagement-Metriken (Verweildauer, Absprung, Scrolltiefe) davon abhängen, ein sauberes Signal für das Sitzungsende einzufangen. Macht man es falsch, verliert man Daten oder bläht sie auf.
Monoid löst das ohne Cookies, localStorage oder irgendeinen persistenten Identifikator. Diese Beschränkung vereinfacht die Sache sogar: Wir müssen eine Sitzung nie über Seitenaufrufe hinweg zusammenführen, es zählt also nur, pro Seitenlebenszyklus genau ein korrektes Abschluss-Beacon zu senden. So verhalten sich die Browser-Primitive — und hier liegen die Fallen.
Warum unload das falsche Event ist
Historisch lauschte man auf unload oder beforeunload und feuerte ein synchrones XHR ab. Das ist heute aktiv schädlich. Das unload-Event ist auf Mobilgeräten unzuverlässig: Wenn ein Tab in den Hintergrund wandert und das Betriebssystem ihn zurückfordert, feuert unload häufig nie. Schlimmer noch: Ein registrierter unload-Handler disqualifiziert die Seite in mehreren Browsern vom Back/Forward-Cache (bfcache) und schadet damit der Navigationsleistung.
Die Anleitung zur Page Lifecycle API von web.dev ist eindeutig: Behandeln Sie den Übergang von visibilitychange zu hidden als das letzte verlässliche Event, das Sie beobachten werden. Verlassen Sie sich nicht darauf, dass unload oder pagehide auf Mobilgeräten feuern.
Das Paar visibilitychange + pagehide
Das robuste Muster ist, auf visibilitychange zu lauschen und document.visibilityState === 'hidden' zu prüfen, ergänzt um pagehide als sekundäres Signal. Sobald die Seite in den verborgenen Zustand wechselt, schreiben Sie alles Ausstehende weg. Die MDN-Dokumentation zur Page Visibility API bestätigt, dass visibilitychange feuert, wenn ein Tab in den Hintergrund geht oder der Browser minimiert wird — der beste verfügbare Näherungswert für „die Person schaut nicht mehr hin".
Die Komplikation: Der Wechsel zu verborgen kann in einer Sitzung mehrfach feuern (wegwechseln, zurückwechseln, wieder wegwechseln). Sie müssen Ihr Wegschreiben also idempotent machen, entprellen oder nur das Delta seit dem letzten Mal senden. Da Monoid keinerlei Zustand pro Person hält, senden wir eine einzige, in sich geschlossene Nutzlast und lassen die Edge anfragebezogen deduplizieren.
Daten von einer sterbenden Seite senden: navigator.sendBeacon
Sie können während visibilitychange kein asynchrones fetch starten und erwarten, dass es abgeschlossen wird — der Browser darf die Seite vorher abbauen. Genau dafür existiert die Spezifikation der Beacon API. navigator.sendBeacon(url, data) stellt ein kleines POST in die Warteschlange, dessen Versuch der Browser auch nach dem Ende des Dokuments garantiert — ohne den Haupt-Thread zu blockieren oder die Navigation zu verzögern.
Beacon hat Grenzen, die es zu respektieren lohnt:
- Nutzlastgröße. Die Spezifikation erlaubt dem User Agent, Beacons oberhalb einer implementierungsdefinierten Grenze (üblicherweise 64 KB) abzulehnen. Halten Sie Nutzlasten winzig — die von Monoid sind wenige hundert Bytes groß.
- Methode. Beacons sind immer POST. Wenn Ihr Edge-Endpunkt GET erwartet, passen Sie ihn an.
- Keine Antwort. Sie können nichts zurücklesen. Abschicken und vergessen.
Eine moderne Ausweichlösung ist fetch(url, { keepalive: true }), das der Fetch-Standard so definiert, dass eine Anfrage das Dokument überleben darf. keepalive gibt Ihnen Header und Methoden, die Beacon fehlen, teilt aber dieselbe Größenobergrenze. Nutzen Sie zuerst Beacon und weichen Sie auf fetch mit keepalive aus.
Eine minimale, cookiefreie Implementierung
let sent = false;
function flush() {
if (sent) return;
sent = true;
const payload = JSON.stringify({
path: location.pathname,
// keine Identifikatoren, keine Cookies, kein Fingerprint
visibleMs: Math.round(performance.now())
});
const ok = navigator.sendBeacon('/collect', payload);
if (!ok) {
fetch('/collect', { method: 'POST', body: payload, keepalive: true });
}
}
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') flush();
});
window.addEventListener('pagehide', flush);
Beachten Sie, was fehlt: kein unload-Listener, damit bleibt die bfcache-Eignung erhalten. Keine über Seitenaufrufe gespeicherte ID. Der Wert visibleMs nutzt performance.now(), eine monotone Uhr, die weder einen Wanduhr-Zeitstempel noch persistenten Zustand braucht.
Warum das zu Privacy-First-Analytik passt
Weil Monoid Sitzungen nie zusammennäht, ist jedes Beacon eine vollständige, wegwerfbare Beobachtung. Es gibt keinen seitenübergreifenden Identifikator zu schützen, ein verlorenes Beacon kostet Sie also einen Datenpunkt und kein beschädigtes Nutzerprofil. Das Muster aus visibilitychange plus Beacon liefert eine genaue Verweildauermessung und hält die Seite zugleich schnell und bfcache-freundlich — was selbst die Core Web Vitals verbessert, die Sie womöglich messen wollen.
Messen Sie das Gehen, nicht die Person. Der Browser gibt Ihnen bereits alles, was Sie brauchen.
Comments
Loading comments…