Bei mehreren Gelegenheiten musste ich eine `HTTP`-Anfrage mit einigen Daten senden, um zu protokollieren, wenn ein Benutzer etwas tut, wie z. B. zu einer anderen Seite navigiert oder ein Formular absendet. Betrachten Sie dieses konstruierte Beispiel, um einige Informationen an einen externen Dienst zu senden, wenn auf einen Link geklickt wird.
<a href="/some-other-page" id="link">Go to Page</a>
<script>
document.getElementById('link').addEventListener('click', (e) => {
fetch("/log", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
some: "data"
})
});
});
</script>
Hier passiert nichts besonders Kompliziertes. Der Link darf sich wie gewohnt verhalten (ich verwende kein `e.preventDefault()`), aber bevor dieses Verhalten eintritt, wird bei einem `click` eine `POST`-Anfrage ausgelöst. Es besteht keine Notwendigkeit, auf eine Antwort zu warten. **Ich möchte nur, dass sie gesendet wird** an welchen Dienst auch immer ich anpinge.
Auf den ersten Blick würden Sie vielleicht erwarten, dass die Zustellung dieser Anfrage synchron erfolgt, danach navigieren wir weiter von der Seite weg, während ein anderer Server diese Anfrage erfolgreich bearbeitet. Aber wie sich herausstellt, ist das nicht immer der Fall.
Browser garantieren nicht, offene HTTP-Anfragen zu erhalten
Wenn etwas passiert, um eine Seite im Browser zu beenden, gibt es keine Garantie, dass eine laufende `HTTP`-Anfrage erfolgreich sein wird (siehe mehr über die "beendeten" und andere Zustände des Seitenlebenszyklus unter hier). Die Zuverlässigkeit dieser Anfragen kann von mehreren Faktoren abhängen – Netzwerkverbindung, Anwendungsleistung und sogar die Konfiguration des externen Dienstes selbst.
Daher kann das Senden von Daten in diesen Momenten alles andere als zuverlässig sein, was ein potenziell erhebliches Problem darstellt, wenn Sie sich auf diese Protokolle verlassen, um datenintensive Geschäftsentscheidungen zu treffen.
Um diese Unzuverlässigkeit zu veranschaulichen, habe ich eine kleine Express-Anwendung mit einer Seite eingerichtet, die den oben genannten Code verwendet. Wenn auf den Link geklickt wird, navigiert der Browser zu `/other`, aber bevor dies geschieht, wird eine `POST`-Anfrage ausgelöst.
Während alles geschieht, habe ich den Netzwerk-Tab des Browsers geöffnet und verwende eine "Langsame 3G"-Geschwindigkeit. Sobald die Seite geladen ist und ich das Protokoll gelöscht habe, sieht es ziemlich ruhig aus.

Aber sobald auf den Link geklickt wird, geht alles schief. Wenn Navigation stattfindet, wird die Anfrage abgebrochen.

Und das lässt uns wenig Vertrauen darin haben, dass der externe Dienst die Anfrage tatsächlich verarbeiten konnte. Nur um dieses Verhalten zu überprüfen, tritt es auch auf, wenn wir programmatisch mit `window.location` navigieren.
document.getElementById('link').addEventListener('click', (e) => {
+ e.preventDefault();
// Request is queued, but cancelled as soon as navigation occurs.
fetch("/log", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
some: 'data'
}),
});
+ window.location = e.target.href;
});
Unabhängig davon, wie oder wann die Navigation stattfindet und die aktive Seite beendet wird, sind diese unvollständigen Anfragen dem Risiko ausgesetzt, abgebrochen zu werden.
Aber warum werden sie abgebrochen?
Die Wurzel des Problems ist, dass XHR-Anfragen (über `fetch` oder `XMLHttpRequest`) standardmäßig asynchron und nicht blockierend sind. Sobald die Anfrage in die Warteschlange gestellt wird, wird die eigentliche *Arbeit* der Anfrage im Hintergrund an eine Browser-API übergeben.
In Bezug auf die Leistung ist das gut – Sie möchten nicht, dass Anfragen den Hauptthread blockieren. Es besteht aber auch die Gefahr, dass sie verworfen werden, wenn eine Seite in diesen "beendeten" Zustand übergeht, was keine Garantie dafür bietet, dass die im Hintergrund laufende Arbeit abgeschlossen wird. Hier ist, wie Google diesen spezifischen Lebenszyklusstatus zusammenfasst.
Eine Seite befindet sich im beendeten Zustand, sobald sie mit dem Entladen begonnen hat und vom Browser aus dem Speicher gelöscht wurde. In diesem Zustand können keine neuen Aufgaben gestartet werden, und laufende Aufgaben können beendet werden, wenn sie zu lange dauern.
Kurz gesagt, der Browser ist mit der Annahme konzipiert, dass beim Schließen einer Seite keine Notwendigkeit besteht, Hintergrundprozesse fortzusetzen, die von ihr in die Warteschlange gestellt wurden.
Was sind also unsere Optionen?
Vielleicht ist der offensichtlichste Ansatz, dieses Problem zu vermeiden, die Benutzeraktion so weit wie möglich zu verzögern, bis die Anfrage eine Antwort zurückgibt. In der Vergangenheit wurde dies auf die falsche Weise durch die Verwendung des synchronen Flags, das in `XMLHttpRequest` unterstützt wird, erreicht. Aber dessen Verwendung blockiert den Hauptthread vollständig und verursacht eine Reihe von Leistungsproblemen – ich habe in der Vergangenheit über einige davon geschrieben –, daher sollte die Idee überhaupt nicht in Betracht gezogen werden. Tatsächlich ist sie bereits auf dem Weg aus der Plattform (Chrome v80+ hat sie bereits entfernt).
Stattdessen ist es besser, auf die Auflösung eines `Promise` zu warten, wenn eine Antwort zurückgegeben wird. Danach können Sie das Verhalten sicher ausführen. Mit unserem früheren Snippet könnte das etwa so aussehen.
document.getElementById('link').addEventListener('click', async (e) => {
e.preventDefault();
// Wait for response to come back...
await fetch("/log", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
some: 'data'
}),
});
// ...and THEN navigate away.
window.location = e.target.href;
});
Das erledigt die Aufgabe, aber es gibt einige nicht unerhebliche Nachteile.
**Erstens beeinträchtigt es die Benutzererfahrung, indem es das gewünschte Verhalten verzögert.** Das Sammeln von Analysedaten nützt sicherlich dem Unternehmen (und hoffentlich zukünftigen Benutzern), aber es ist weniger als ideal, Ihre *gegenwärtigen* Benutzer die Kosten tragen zu lassen, um diese Vorteile zu realisieren. Ganz zu schweigen davon, dass bei externen Abhängigkeiten Latenzzeiten oder andere Leistungsprobleme des Dienstes selbst für den Benutzer sichtbar werden. Wenn Timeouts von Ihrem Analysedienst dazu führen, dass ein Kunde eine hochwertige Aktion nicht abschließen kann, verliert jeder.
**Zweitens ist dieser Ansatz nicht so zuverlässig, wie er zunächst klingt, da einige Abbruchverhaltensweisen nicht programmatisch verzögert werden können.** Zum Beispiel ist `e.preventDefault()` nutzlos, um jemanden vom Schließen eines Browser-Tabs abzuhalten. Daher wird er im besten Fall das Sammeln von Daten für *einige* Benutzeraktionen abdecken, aber nicht genug, um ihm umfassend vertrauen zu können.
Dem Browser anweisen, ausstehende Anfragen zu erhalten
Glücklicherweise gibt es Optionen, um ausstehende `HTTP`-Anfragen zu *erhalten*, die in den meisten Browsern integriert sind und keine Beeinträchtigung der Benutzererfahrung erfordern.
Verwendung des `keepalive`-Flags von Fetch
Wenn das `keepalive`-Flag beim Verwenden von `fetch()` auf `true` gesetzt ist, bleibt die entsprechende Anfrage geöffnet, auch wenn die Seite, die sie initiiert hat, beendet wird. Unter Verwendung unseres ursprünglichen Beispiels wäre dies eine Implementierung, die so aussieht.
<a href="/some-other-page" id="link">Go to Page</a>
<script>
document.getElementById('link').addEventListener('click', (e) => {
fetch("/log", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
some: "data"
}),
keepalive: true
});
});
</script>
Wenn auf diesen Link geklickt wird und die Seitenavigation erfolgt, findet keine Anfrageabbruch statt.

Stattdessen bleiben wir mit einem `(unknown)`-Status zurück, einfach weil die aktive Seite nie darauf gewartet hat, eine Antwort zu erhalten.
Eine Einzeiler wie diese ist eine einfache Lösung, besonders wenn sie Teil einer gängigen Browser-API ist. Aber wenn Sie nach einer fokussierteren Option mit einer einfacheren Oberfläche suchen, gibt es einen anderen Weg mit praktisch derselben Browserunterstützung.
Verwendung von `Navigator.sendBeacon()`
Die Funktion `Navigator.sendBeacon()` ist speziell für das Senden von Einweganfragen (Beacons) vorgesehen (Beacons). Eine einfache Implementierung sieht so aus, wobei ein `POST` mit stringifiziertem JSON und einem "text/plain" `Content-Type` gesendet wird.
navigator.sendBeacon('/log', JSON.stringify({
some: "data"
}));
Aber diese API erlaubt Ihnen nicht, benutzerdefinierte Header zu senden. Um also unsere Daten als "application/json" zu senden, müssen wir eine kleine Anpassung vornehmen und ein `Blob` verwenden.
<a href="/some-other-page" id="link">Go to Page</a>
<script>
document.getElementById('link').addEventListener('click', (e) => {
const blob = new Blob([JSON.stringify({ some: "data" })], { type: 'application/json; charset=UTF-8' });
navigator.sendBeacon('/log', blob));
});
</script>
Am Ende erhalten wir dasselbe Ergebnis – eine Anfrage, die auch nach der Seitenavigation abgeschlossen werden kann. Aber es gibt noch etwas mehr, das ihr einen Vorteil gegenüber `fetch()` verschaffen könnte: Beacons werden mit geringer Priorität gesendet.
Zur Veranschaulichung, hier ist, was im Netzwerk-Tab angezeigt wird, wenn sowohl `fetch()` mit `keepalive` *als auch* `sendBeacon()` gleichzeitig verwendet werden.

Standardmäßig erhält `fetch()` eine "Hohe" Priorität, während der Beacon (oben als "ping"-Typ bezeichnet) die "Niedrigste" Priorität hat. Für Anfragen, die für die Funktionalität der Seite nicht kritisch sind, ist das eine gute Sache. Direkt aus der Beacon-Spezifikation.
Diese Spezifikation definiert eine Schnittstelle, die [...] die Ressourcenkonkurrenz mit anderen zeitkritischen Operationen minimiert und gleichzeitig sicherstellt, dass solche Anfragen dennoch verarbeitet und an ihr Ziel geliefert werden.
Anders ausgedrückt: `sendBeacon()` stellt sicher, dass seine Anfragen die wirklich wichtigen für Ihre Anwendung und die Benutzererfahrung nicht behindern.
Eine ehrenvolle Erwähnung für das `ping`-Attribut
Es ist erwähnenswert, dass eine wachsende Zahl von Browsern das `ping`-Attribut unterstützt. Wenn es an Links angehängt wird, löst es eine kleine `POST`-Anfrage aus.
<a href="https://:3000/other" ping="https://:3000/log">
Go to Other Page
</a>
Und diese Anfrage-Header enthalten die Seite, auf der auf den Link geklickt wurde (`ping-from`) sowie den `href`-Wert dieses Links (`ping-to`).
headers: {
'ping-from': 'https://:3000/',
'ping-to': 'https://:3000/other'
'content-type': 'text/ping'
// ...other headers
},
Es ist technisch ähnlich wie das Senden eines Beacons, hat aber einige bemerkenswerte Einschränkungen.
- Es ist streng auf die Verwendung bei Links beschränkt, was es zu einem Nichtstarter macht, wenn Sie Daten verfolgen müssen, die mit anderen Interaktionen wie Button-Klicks oder Formularübermittlungen verbunden sind.
- Die Browserunterstützung ist gut, aber nicht großartig. Zum Zeitpunkt der Erstellung dieses Artikels ist Firefox standardmäßig nicht aktiviert.
- Sie können keine benutzerdefinierten Daten zusammen mit der Anfrage senden. Wie bereits erwähnt, erhalten Sie höchstens ein paar `ping-*`-Header, zusammen mit allen anderen Headern, die mitgeschickt werden.
Alles in allem ist `ping` ein gutes Werkzeug, wenn Sie damit einverstanden sind, einfache Anfragen zu senden und keinen benutzerdefinierten JavaScript-Code schreiben möchten. Wenn Sie jedoch etwas Substantielleres senden müssen, ist es möglicherweise nicht die beste Wahl.
Welches sollte ich also wählen?
Es gibt definitiv Kompromisse bei der Verwendung von entweder `fetch` mit `keepalive` oder `sendBeacon()` zum Senden Ihrer letzten Anfragen. Um zu entscheiden, welche für verschiedene Umstände am besten geeignet ist, hier einige Überlegungen.
Sie könnten `fetch()` + `keepalive` verwenden, wenn
- Sie benutzerdefinierte Header einfach mit der Anfrage übergeben müssen.
- Sie eine `GET`-Anfrage an einen Dienst senden möchten, anstatt einer `POST`-Anfrage.
- Sie ältere Browser (wie IE) unterstützen und bereits ein `fetch`-Polyfill laden.
Aber `sendBeacon()` könnte eine bessere Wahl sein, wenn
- Sie einfache Service-Anfragen machen, die nicht viel Anpassung benötigen.
- Sie die sauberere, elegantere API bevorzugen.
- Sie sicherstellen möchten, dass Ihre Anfragen nicht mit anderen hoch priorisierten Anfragen in der Anwendung konkurrieren.
Vermeiden Sie es, meine Fehler zu wiederholen
Es gibt einen Grund, warum ich mich entschieden habe, die Natur, wie Browser laufende Anfragen behandeln, wenn eine Seite beendet wird, eingehend zu untersuchen. Vor einiger Zeit sah mein Team eine plötzliche Änderung in der Häufigkeit einer bestimmten Art von Analyseprotokoll, nachdem wir die Anfrage gerade dann ausgelöst hatten, wenn ein Formular abgeschickt wurde. Die Änderung war abrupt und signifikant – ein Rückgang von ~30% gegenüber dem, was wir historisch gesehen hatten.
Die Gründe, warum dieses Problem auftrat, sowie die verfügbaren Werkzeuge, um es erneut zu vermeiden, haben den Tag gerettet. Wenn also irgendetwas, hoffe ich, dass das Verständnis der Nuancen dieser Herausforderungen jemandem hilft, einige der Schwierigkeiten zu vermeiden, auf die wir gestoßen sind. Viel Spaß beim Protokollieren!
Was ist mit der Verwendung eines Service Workers Fetch und
der Background Sync API, um Anfragen im Hintergrund unabhängig vom Seitenstatus auszuführen?
Tatsächlich garantiert dies sogar, dass die Anfrage gestellt wird, erfordert aber HTTPS.
Service Worker sind dafür großartig. Insbesondere da sie Zugriff auf alle HTTP-Anfragen haben, was es ermöglicht, sie unabhängig vom UI-Code zu protokollieren.
Beachten Sie, dass sendBeacon in etwa 96 % der Browser verfügbar ist, während fetch mit keepalive heute nur in etwa 80 % verfügbar ist.
Großartiger Artikel!
Kann man mit der Beacon API Authentifizierungsheader übergeben?
Eine Sache jedoch, als ich das letzte Mal versucht habe, funktionierte Beacon auf Mobilgeräten nicht :/
InstantPage Js wäre sehr hilfreich bei der Geschwindigkeitsfrage.
https://instant.page/
Ich verwende Axios als mein HTTP-Anforderungsbibliothek auf der Clientseite und muss benutzerdefinierte Auth-Header senden. Es scheint, als wäre dies eine ziemlich schwierigere Implementierung.
Wenn fetch für Anfragen mit geringer Priorität verwendet wird, könnte die Wichtigkeit entsprechend eingestellt werden.
Warte fetch("/log", {importance: "low", keepalive: true, ...});`{importance: "low"}` in fetch init erfordert Chrome 101.
https://web.dev/priority-hints/
Vielen Dank für den tollen Artikel. Ich sprach gerade mit einigen Leuten von der Arbeit darüber. Das adressiert die Sorge, die ich hatte, dass eine Anfrage abgebrochen wird, nachdem ein Benutzer eine Seite verlässt.
Ich möchte nur zu diesem Punkt bezüglich der Verwendung von fetch mit keepalive kommentieren
* Sie unterstützen ältere Browser (wie IE) und haben bereits ein fetch Polyfill geladen.Tun Sie das? Ältere Browser mit einem Fetch-Polyfill können keine Anfrage am Leben erhalten, da ein Polyfill kein magischer Browsercode ist, sondern nur JavaScript-Wrapper für vorhandene Funktionalität, also widerspricht es dem Zweck.
Decken alle diese Methoden den Anwendungsfall des Schließens des Browsers ab? Soweit ich weiß, kann sendBeacon nicht in Seitenentladehandlern verwendet werden...
Nein, sendBeacon() ist genau dafür gedacht, Analyse-Daten im letzten Moment von einer Seite zu erfassen. Wahrscheinlich funktioniert es in **unload()**. Wenn nicht, verwenden Sie **beforeunload()**; das funktioniert. Ich habe es getestet und gesehen, dass es funktioniert, indem ich diese Beacon API in der timeonsite.js Bibliothek getestet habe; sie hängt vollständig von sendBeacon() für die Echtzeit-Datenerfassung ab. Es scheint revolutionär zu sein.
Ich bin neugierig auf eine abgebrochene Anfrage, ob sie angekommen ist oder nicht.
Hängt das nur vom Internetstatus ab?
Wenn es keine Engpässe gibt, kann es ankommen.
Wenn es einen Engpass gibt, kann es nicht ankommen, weil TCP getrennt wird.
Es gibt derzeit einen Vorschlag für einen Beacon, der garantiert geliefert wird, auch wenn die Anzeige verschwindet.
https://github.com/darrenw/docs/blob/main/explainers/beacon_api.md
Haben Sie versucht, einen WebSocket zu öffnen? WebSockets sind persistente Verbindungen. Beide Seiten können erkennen, wenn die andere die Leitung trennt. Sie können dies verwenden, um zu erkennen, ob eine Seite geändert wurde oder ob der Tab geschlossen wurde.
Warum gibt es keine Erwähnung von unload/beforeunload?
Ich denke, es ist erwähnenswert, dass ein Fetch-Polyfill wahrscheinlich eine synchrone XHR-Anfrage verwendet, anstatt keepalive zu unterstützen.
Ich bin neugierig, welche Polyfills empfohlen werden und ob einige eine Implementierung haben, die Feature-Erkennung für keepalive und send beacon durchführt, bevor sie auf synchrone XHR zurückfällt.
Alex, gibt es eine Beziehung zwischen dem fetch keepAlive-Flag und dem Connection: "keep-alive"-Header (https://www.imperva.com/learn/performance/http-keep-alive/)?
Ich denke, das ist der Grund, warum moderne Analysetracker wie timeonsite.js vollständig von **sendBeacon()** abhängen und nicht von XMLHttpRequest mit dem "sync"-Flag oder der Fetch() API mit dem "keepAlive"-Flag. Es scheint gut performant und sehr stabil für alle Arten von **unload()**-Ereignissen im Browser zu sein. https://saleemkce.github.io/timeonsite/docs/index.html#real-time-example
Hier sind zwei Fragen,
Wenn der Link auf Ihre eigene Website verweist, dann kann der Dienst die Anfrage definitiv empfangen. Warum sollten Sie eine separate Anfrage senden, anstatt direkt über diese Anfrage aufzuzeichnen / zu analysieren?
Wenn der Link eine externe Seite ist, springen jetzt viele Websites zuerst zu einer Zwischenseite auf ihrer eigenen Website und springen dann weiter, damit ihre eigene Website auch aufgezeichnet werden kann. Warum senden Sie eine separate Anfrage?
Wunderbar. Danke dafür!
Sowohl fetch keepalive als auch navigator.sendBeacon sind jetzt großartige Ergänzungen zu meiner Werkzeugkiste.
Warum wird nicht erwähnt, dass Firefox `keepalive` nicht unterstützt? Das scheint ein guter Grund zu sein, manchmal `sendBeacon` zu verwenden :-)
Siehe https://bugzilla.mozilla.org/show_bug.cgi?id=1342484
Danke, Beacons funktionieren großartig.