navigator.sendBeacon vs. fetch keepalive: Zuverlässige Analytics-Übertragung, ohne das Entladen zu blockieren
Wie man Analytics-Payloads sendet, die das Entladen der Seite überstehen, ohne die Navigation zu verzögern — und warum die Wahl zwischen sendBeacon und fetch keepalive für cookiefreie Erfassung entscheidend ist.
Eine Page View oder ein Event zu erfassen ist einfach, solange die Seite lebt. Schwierig wird es, dieses letzte Payload genau dann zuzustellen, wenn der Nutzer wegnavigiert, den Tab schließt oder die App auf dem Handy in den Hintergrund schickt. Macht man das falsch, verliert man entweder Daten oder verzögert schlimmstenfalls genau die Navigation, die der Nutzer angefordert hat. Dieser Beitrag vergleicht die beiden ernstzunehmenden Optionen — navigator.sendBeacon() und fetch() mit keepalive: true — und erklärt, welche davon Monoids Tracker tatsächlich einsetzt und warum, ganz ohne Cookies, Storage oder Fingerprinting.
Warum das Entladen ein Sonderfall ist
Wenn ein Dokument entladen wird, fährt der Browser gerade herunter. Synchrones XMLHttpRequest wurde hier historisch missbraucht, um die Navigation zu blockieren, bis eine Anfrage abgeschlossen war — das schadet der Reaktionsfähigkeit. Moderne Browser unterbinden das aktiv, und während unload gestartete Anfragen werden häufig abgebrochen. Genau dafür gibt es die Beacon-Spezifikation: Sie erlaubt einer Seite, eine Anfrage einzuplanen, deren Zustellung der User Agent asynchron garantiert versucht — ohne das Laden der nächsten Seite zu blockieren.
Die praktische Konsequenz für Analytics: Verlasst euch nie auf einen normalen fetch oder XHR, der aus einem beforeunload/unload-Handler heraus ausgelöst wird. Er verlässt die Maschine womöglich einfach nie.
navigator.sendBeacon
sendBeacon ist genau dafür gebaut. Es reiht einen kleinen POST ein und liefert sofort einen Boolean zurück, der nur aussagt, ob die Anfrage erfolgreich eingereiht wurde — nicht, ob sie erfolgreich war. Der Browser überträgt sie dann im Hintergrund, selbst nachdem die Seite bereits verschwunden ist.
const ok = navigator.sendBeacon('/collect', payload);
Stärken:
- Die Übertragung ist nicht an die Lebensdauer des Dokuments gebunden.
- Sie hat niedrige Priorität und ist darauf ausgelegt, nicht mit der nächsten Navigation zu konkurrieren.
- Es gibt keine Antwort zu verarbeiten, also nichts, worauf man warten müsste.
Einschränkungen laut Spezifikation und Browser-Implementierungen:
- Nur POST. Beliebige Methoden lassen sich nicht setzen.
- Begrenzte Kontrolle über Header. Der
Content-Typewird aus dem Payload-Typ abgeleitet (einBloberlaubt zwar etwas Einfluss darauf, aber eigene Request-Header lassen sich nicht frei setzen). - Payloads zählen gegen ein Datenlimit des User Agents; überdimensionierte Beacons liefern
falsezurück.
Für cookiefreie Analytics reicht das meistens aus — etliche Tracker setzen genau darauf und lassen es dabei bewenden. Monoid tut das aus einem konkreten Grund nicht: sendBeacon kann mode: 'cors' nicht ausdrücken, und Monoids Installations-Snippet erlaubt es einer Site, data-api-url auf einen anderen Origin als die Host-Seite zeigen zu lassen. Genau diese eine Einschränkung entscheidet den folgenden Vergleich.
fetch mit keepalive
Der Fetch-Standard definiert ein keepalive-Flag, das eine Anfrage über die Seite hinaus am Leben hält, die sie ausgelöst hat — das liefert die Zuverlässigkeit von sendBeacon mit einer wesentlich reichhaltigeren API.
fetch('/collect', {
method: 'POST',
keepalive: true,
headers: { 'Content-Type': 'application/json' },
body: payload,
});
Stärken:
- Volle Kontrolle über Methode, Header und Body.
- Eine echte
Response, die man inspizieren kann (auch wenn man während des Entladens meist nicht darauf warten sollte).
Einschränkungen:
- Die Fetch-Spezifikation begrenzt die Gesamtgröße des Bodys aller gleichzeitig laufenden keepalive-Anfragen auf 64 KiB pro Dokument. Wird das überschritten, schlägt der fetch fehl. Es handelt sich um ein Budget pro Dokument, das sich mehrere gleichzeitige keepalive-Anfragen teilen.
- Historisch gesehen hinkte die
keepalive-Unterstützung der vonsendBeaconhinterher, und es gab Implementierungseigenheiten. Testet auf den Browsern, die eure Zielgruppe tatsächlich nutzt.
Genau das setzt Monoids Tracker für jede Anfrage ein — /collect, /duration und /event gleichermaßen:
fetch(endpoint, {
method: 'POST',
body: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' },
keepalive: true,
mode: 'cors',
});
mode: 'cors' ist der entscheidende Faktor aus dem vorigen Abschnitt, nicht die Payload-Größe oder die Anzahl der Header — das lohnt sich zu merken, da die meisten Beiträge die Wahl rein anhand der Größe einordnen.
Was man wählen sollte
Eine pragmatische Faustregel: sendBeacon für Fire-and-Forget-Telemetrie einsetzen, bei der man nichts zurückbraucht, und nur dann zu fetch mit keepalive greifen, wenn man wirklich eigene Header, eine größere strukturierte Anfrage oder Kontrolle über den mode bei Cross-Origin-Zugriffen benötigt. Payloads sollten so oder so klein bleiben — deutlich unter dem 64-KiB-Budget von keepalive —, weil Analytics-Events per Design minimal sein sollten. Datenminimierung ist nicht nur eine Compliance-Haltung; sie ist genau das, was zuverlässige Zustellung beim Entladen überhaupt erst möglich macht. Monoid ist die Ausnahme, die die Regel bereits einkalkuliert: Das Payload ist klein und die Antwort wird nie gelesen — genau das Profil, für das sendBeacon gebaut wurde — aber die Unterstützung von mode: 'cors' gibt am Ende den Ausschlag.
Auf welches Event man wirklich hören sollte
Hängt euch nicht an unload. Es ist unzuverlässig und verhindert den Einsatz des Back/Forward-Cache. Die Page-Lifecycle-Empfehlung rät dazu, auf visibilitychange zu hören und den Übergang zu hidden als letzten verlässlichen Moment zum Abschicken zu behandeln. Das funktioniert konsistent über Desktop-Schließen, Tab-Wechsel und das Backgrounding mobiler Apps hinweg — Fälle, in denen unload nie ausgelöst wird.
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') flush();
});
Monoids Tracker folgt diesem Muster: Ein visibilitychange-Listener steht neben einem beforeunload-Fallback, sodass eine Dauer-Stichprobe gemeldet wird, egal ob ein Tab auf dem Handy in den Hintergrund geschickt oder auf dem Desktop komplett geschlossen wird.
Wie das zu einem cookiefreien Modell passt
Da Monoid nie Cookies oder Storage ausliest und nie Fingerprinting betreibt, trägt jede Anfrage nur das, was sie ohnehin schon preisgibt — ein grobes Event plus die Header, die der Browser sowieso mitschickt. Es gibt keinen Identifier, den man persistieren müsste, also brauchen wir nie einen bidirektionalen Austausch, um Zustand zu synchronisieren. Genau das macht fetch mit keepalive zur naheliegenden Wahl, sobald mode: 'cors' sendBeacon aus dem Rennen nimmt: ein einmaliger POST, dessen Antwort wir nie inspizieren, ist für uns keine Einschränkung — er bringt Zustellzuverlässigkeit und Privacy by Design in Einklang.
Comments
Loading comments…