TL;DR, Réponse rapide
8 min de lectureL'écart entre sendBeacon vs fetch keepalive tient au contrôle, pas à la livraison. navigator.sendBeacon() met en file un POST qui survit à la page et ne renvoie que true ou false, avec un Content-Type que le navigateur déduit du type de corps que vous passez. fetch() avec keepalive à true ajoute des en-têtes personnalisés, d'autres méthodes et la réponse, et les deux puisent dans le même budget en vol de 64 kibioctets défini par le standard WHATWG Fetch.
Quelle est la différence entre sendBeacon vs fetch keepalive ?
La différence concrète entre sendBeacon vs fetch keepalive tient au contrôle : navigator.sendBeacon() met en file un POST qui survit à la page mais ne renvoie rien d'autre que true ou false, tandis que fetch() avec keepalive: true vous laisse choisir la méthode, définir des en-têtes personnalisés et lire la réponse du serveur. La spécification Beacon du W3C est explicite : "The sendBeacon() method does not provide ability to customize the request method, provide custom request headers, or change other processing properties of the request and response. Applications that require non-default settings for such requests should use the [FETCH] API with keepalive set to true." Les deux passent par la même machinerie fetch en dessous, donc une seule limite de taille les gouverne tous les deux.
Combien de données sendBeacon vs fetch keepalive peuvent-ils envoyer ?
Une requête keepalive transporte au plus 64 kibioctets de corps, soit 65 536 octets. Le standard WHATWG Fetch applique le plafond à l'intérieur de HTTP-network-or-cache fetch, avant que la requête n'atteigne le réseau :
If contentLength is non-null and httpRequest's keepalive is true, then:
- Let inflightKeepaliveBytes be 0.
- Let group be httpRequest's client's fetch group.
- Let inflightRecords be the set of fetch records in group whose request's keepalive is true and done flag is unset.
- For each fetchRecord of inflightRecords:
- Let inflightRequest be fetchRecord's request.
- Increment inflightKeepaliveBytes by inflightRequest's body's length.
- If the sum of contentLength and inflightKeepaliveBytes is greater than 64 kibibytes, then return a network error.
La note de la spécification explique le chiffre : "The above limit ensures that requests that are allowed to outlive the environment settings object and contain a body, have a bounded size and are not allowed to stay alive indefinitely." Mesurez la charge sérialisée en octets, pas en caractères, avant de lui faire confiance pour tenir.
Le plafond de 64 KiB est-il partagé entre les requêtes keepalive en vol ?
Le plafond est partagé, pas par appel. L'étape 5 ci-dessus additionne contentLength et inflightKeepaliveBytes, la longueur totale du corps de chaque requête keepalive inachevée du groupe de fetch. La spécification Beacon dit la même chose de son côté : "Requests initiated via the Beacon API automatically set the keepalive flag, and developers can similarly set the same flag manually when using the Fetch API. All requests with this flag set share the same in-flight quota restrictions that is enforced within the Fetch API." Un appel sendBeacon() et un appel fetch(..., { keepalive: true }) se disputent un seul budget.
La marge qui reste pour votre prochain appel vaut :
remaining bytes = 65536 - (sum of body lengths of unfinished keepalive requests)
Imaginons un rapport d'erreur de 12 288 octets et un lot de clics de 8 192 octets, tous deux en vol. La marge vaut 65 536 moins 20 480, soit 45 056 octets. Un résumé de session de 50 000 octets pousse la somme à 70 480, donc fetch() renvoie une erreur réseau et sendBeacon() renvoie false. Regroupez chaque charge de suivi des événements dans un seul envoi et ce calcul cesse de mordre.
Quel Content-Type sendBeacon associe-t-il à chaque type de corps ?
sendBeacon ne vous laisse jamais définir Content-Type vous-même : le navigateur le déduit du type de corps que vous passez, et cette valeur décide si la requête reste en mode no-cors ou déclenche un preflight CORS. Le modèle de traitement de la spécification Beacon détaille l'embranchement :
If contentType is not null:
- Set corsMode to "cors".
- If contentType value is a CORS-safelisted request-header value for the
Content-Typeheader, set corsMode to "no-cors".- Append a
Content-Typeheader with value contentType to headerList.
Le standard Fetch définit le test de safelist pour cet en-tête : "If mimeType's essence is not application/x-www-form-urlencoded, multipart/form-data, or text/plain, then return false." Fetch fixe aussi le Content-Type que produit chaque type de corps :
| Corps passé à sendBeacon | Content-Type défini par le navigateur | Sur la safelist CORS | Résultat cross-origin |
|---|---|---|---|
| ArrayBuffer, TypedArray, DataView | aucun | aucun en-tête envoyé | no-cors, pas de preflight |
| String | text/plain;charset=UTF-8 | Oui | no-cors, pas de preflight |
| URLSearchParams | application/x-www-form-urlencoded;charset=UTF-8 | Oui | no-cors, pas de preflight |
| FormData | multipart/form-data; boundary=... | Oui | no-cors, pas de preflight |
Blob de type application/json | application/json | Non | mode CORS, preflight requis |
La spécification Beacon énonce la conséquence : "If the request does not contain a payload, or the request Content-Type is a CORS-safelisted request-header, then the request mode is no-cors." Sinon, selon les termes de la spécification, "a CORS preflight is made and the server needs to first allow such requests by returning the appropriate set of CORS headers: Access-Control-Allow-Credentials, Access-Control-Allow-Origin, Access-Control-Allow-Headers." Un preflight double les allers-retours sur une page qui est déjà en train de se décharger, alors envoyez le JSON sous forme de chaîne simple et parsez-le côté serveur.
![]()
Pourquoi un beacon doit-il se déclencher sur pagehide plutôt que sur unload ?
Déclenchez le beacon d'abord sur visibilitychange et ensuite sur pagehide, et laissez unload tranquille, parce que unload casse le cache avant/arrière. La page sendBeacon de MDN le dit sans détour : "the unload event is incompatible with the back/forward cache (bfcache) implemented in modern browsers. Some browsers, such as Firefox, handle this incompatibility by excluding pages from the bfcache if they contain unload handlers, thus hurting performance." MDN recommande pagehide comme repli pour les navigateurs sans visibilitychange : "Like beforeunload and unload, this event is not reliably fired, especially on mobile. However, it is compatible with the bfcache."
La spécification Beacon ajoute la raison mobile : "Developers should avoid relying on unload event because it will not fire whenever a page is in a background state (i.e. visibilityState equal to hidden) and the process is terminated by the mobile OS." L'arbitrage complet entre les événements de sortie se trouve dans beforeunload vs pagehide.
Tableau de bord en temps réel
Suivi des objectifs
Suivi sans cookies
Comment écrire un appel sendBeacon qui survit au déchargement ?
Passez une chaîne simple, vérifiez le booléen renvoyé, et ne videz le tampon qu'une fois que le navigateur a accepté la file :
const endpoint = "https://collect.example.com/session";
const buffer = { sessionId: crypto.randomUUID(), events: [] };
function flush() {
if (buffer.events.length === 0) return;
const body = JSON.stringify(buffer);
if (navigator.sendBeacon(endpoint, body)) {
buffer.events.length = 0;
}
}
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") flush();
});
window.addEventListener("pagehide", flush);Le corps de type chaîne produit text/plain;charset=UTF-8, qui figure sur la safelist, donc le POST cross-origin saute le preflight. Un retour false signifie que la charge plus tout ce qui est en vol a dépassé 65 536 octets, et le tampon survit pour la tentative suivante.
Comment écrire le même appel avec fetch keepalive ?
Mettez keepalive: true, ajoutez les en-têtes que sendBeacon ne transportera pas, et acceptez que la promesse ne se résolve que si la page vit assez longtemps pour le voir :
window.addEventListener("pagehide", () => {
fetch("https://collect.example.com/session", {
method: "POST",
keepalive: true,
headers: {
"Content-Type": "application/json",
"X-Api-Key": apiKey,
},
body: JSON.stringify(buffer),
}).catch(() => {});
});Content-Type: application/json comme l'en-tête personnalisé X-Api-Key sortent de la safelist CORS, donc un appel cross-origin déclenche ici un preflight OPTIONS et exige Access-Control-Allow-Headers sur le serveur. Une restriction ne vaut que pour keepalive : l'algorithme extract-a-body du standard Fetch lève un TypeError pour un corps ReadableStream quand le drapeau est posé, donc le streaming à la sortie n'est pas une option. Ce moment de sortie explique pourquoi un script léger justifie sa place, et pourquoi le surcoût de performance du session replay apparaît à l'envoi, pas à l'enregistrement.
Lequel faut-il utiliser ?
Utilisez sendBeacon pour les analytics qu'on envoie et qu'on oublie, et fetch keepalive dès qu'un en-tête, une méthode ou la réponse compte.
| Besoin | sendBeacon | fetch keepalive |
|---|---|---|
| Méthode HTTP | POST uniquement | n'importe quelle méthode |
| En-têtes de requête personnalisés | non pris en charge | pris en charge |
| Contrôle du Content-Type | déduit du type de corps | défini par vous |
| Réponse du serveur | indisponible | lue via la promesse |
| Taille du corps | partage le budget de 64 KiB | partage le même budget |
| Disponible dans les service workers | Non | Oui |
Au-delà de 64 kibioctets, aucun des deux ne marche : échantillonnez la charge, découpez-la, ou déplacez l'agrégation vers le serveur, un choix traité dans analytics côté client et côté serveur.
![]()
Questions fréquentes
Une valeur de retour true de sendBeacon signifie-t-elle que le serveur a reçu les données ?
Non. La spécification Beacon dit qu'un retour true "implies the browser has queued the data for transfer" et que "since the actual data transfer happens asynchronously, this method does not provide any information whether the data transfer has succeeded or not." Un retour false est le seul autre signal, et il veut dire que la file a rejeté la charge.
Puis-je définir un en-tête Authorization sur sendBeacon ?
Non. La spécification Beacon indique que sendBeacon "does not provide ability to customize the request method, provide custom request headers, or change other processing properties of the request and response." Déplacez l'identifiant dans le chemin de l'URL, dans un cookie ou dans le corps, ou passez à fetch() avec keepalive: true.
Pourquoi mon Blob sendBeacon déclenche-t-il un preflight CORS ?
Parce que le type du Blob devient le Content-Type de la requête, et application/json ne figure pas sur la safelist CORS. Le standard Fetch limite cette safelist à application/x-www-form-urlencoded, multipart/form-data et text/plain, donc tout le reste force un preflight OPTIONS. Envoyez le JSON sous forme de chaîne et la requête reste en no-cors.
La limite de 64 KiB est-elle par requête ou par page ?
Par groupe de fetch, sur chaque requête keepalive inachevée qu'il contient. Le standard Fetch additionne le contentLength du nouveau corps et inflightKeepaliveBytes puis renvoie une erreur réseau au-delà de 64 kibioctets, donc un gros beacon encore en vol réduit la place pour le suivant.
Est-ce que fetch keepalive fonctionne dans un service worker ?
Oui. MDN cite la disponibilité dans les service workers comme un avantage de fetch() avec keepalive sur sendBeacon(), aux côtés des méthodes autres que POST, des propriétés de requête personnalisées et de l'accès à la réponse.
Qu'arrive-t-il à une requête keepalive si l'utilisateur ferme l'onglet immédiatement ?
Le navigateur garde la requête en vie au-delà du document qui l'a lancée, ce qui est l'objet du drapeau. Le standard Fetch décrit keepalive comme permettant "the request to outlive the environment settings object," et le plafond existe pour que ces requêtes survivantes "have a bounded size and are not allowed to stay alive indefinitely." Ce que vous perdez, c'est la réponse : la promesse n'a plus de page vivante où se résoudre.
sendBeacon peut-il utiliser une méthode autre que POST ?
sendBeacon n'envoie que des requêtes POST, comme le montre le tableau comparatif. fetch avec keepalive accepte n'importe quelle méthode, donc passez à fetch quand l'endpoint attend PUT, PATCH ou GET.
Pourquoi un corps en streaming échoue-t-il avec fetch keepalive ?
L'algorithme extract-a-body de la norme Fetch lève une TypeError pour un corps ReadableStream dès que keepalive est activé, un flux ne peut donc pas quitter la page au moment du départ. Sérialisez la charge avec JSON.stringify ou passez plutôt un corps de type chaîne.
Tableau de bord en temps réel
Suivi des objectifs
Suivi sans cookies
Faut-il écouter pagehide et visibilitychange ensemble, ou choisir l'un des deux ?
Déclenchez d'abord sur visibilitychange, car cet événement capture le passage d'un onglet en arrière-plan avant que la page ne se décharge réellement, puis ajoutez pagehide comme repli pour les navigateurs où visibilitychange ne se déclenche pas de façon fiable. La fonction flush de l'exemple de l'article attache les deux écouteurs et laisse celui qui se déclenche en premier envoyer le beacon.
Que faire une fois que ma charge dépasse 64 KiB ?
Ni sendBeacon ni fetch keepalive ne peuvent transporter un corps au-delà de 65 536 octets ; la requête renvoie une erreur réseau ou un résultat false dès que le budget partagé est dépassé. Échantillonnez la charge, répartissez-la sur plusieurs envois ou déplacez l'agrégation vers le serveur.
Cet article vous a-t-il été utile ?
Dites-nous ce que vous en pensez !
Nous voir plus souvent sur Google
Un clic définit Flowsery comme source préférée. Nos articles remontent alors dans vos À la une, en mode IA et dans les aperçus IA.
Avant de partir...
Flowsery
Des analyses orientées revenus pour votre site web
Suivez chaque visiteur, source et conversion en temps réel. Simple, puissant et sans cookies.
Tableau de bord en temps réel
Suivi des objectifs
Suivi sans cookies
Termes connexes du glossaire


Pourquoi beforeunload vs pagehide décide si vos analytics survivent
Comparer beforeunload vs pagehide montre pourquoi le mobile saute beforeunload, bloque le bfcache et pagehide envoie fiablement les données via sendBeacon.


Pourquoi le CNAME cloaking ne cache plus les traqueurs à Safari ni à Brave
Le CNAME cloaking déguise un traqueur en sous-domaine de votre site. L'astuce DNS, le plafond de 7 jours de Safari, et comment Brave et uBlock le démasquent.


Comment le cookie syncing associe les ID publicitaires d'un site à l'autre
L'ad tech utilise le cookie syncing pour échanger des ID via redirections et pixels. Safari, Firefox et Chrome face à lui, et pourquoi l'analytics s'en passe.


Créer des métriques calculées GA4 avec un quota de cinq emplacements
Google limite les métriques calculées GA4 à 5 par propriété standard. Syntaxe des formules, unités, emplacements et ce qu'une formule ne peut pas utiliser.


Configurer le regroupement de contenu GA4 avec un seul paramètre
Configurez le regroupement de contenu GA4 avec le paramètre content_group dans gtag ou Tag Manager, lisez la dimension Content group et évitez les (not set).


Ce qu'envoient les User-Agent Client Hints, et ce que l'analytics voit encore
Dans Chrome, les User-Agent Client Hints répartissent les données du navigateur en en-têtes Sec-CH-UA. Entropie, Accept-CH, UA réduit et ce que lit l'analytics.
Articles connexes


Ce qui distingue vraiment rage clicks vs dead clicks
La différence rage clicks vs dead clicks porte sur la cause, pas la gravité. Seuils de détection publiés par PostHog, Hotjar, FullStory, LogRocket et Clarity.


Où les réglages par défaut de la censure des PII en relecture de session exposent vos données
Les réglages par défaut de la censure des PII en relecture de session varient : rrweb ne masque que les mots de passe, Sentry masque tout le texte.


Ce que signifie vraiment le trafic Unassigned dans GA4
Un canal sans règle correspondante : le trafic Unassigned dans GA4 n'est pas une valeur absente comme (not set). Les règles de Google font la différence.