Retour au blog

navigator.sendBeacon vs fetch keepalive : livraison fiable des données analytics sans bloquer la navigation

Comment envoyer des payloads analytics qui survivent au déchargement de la page sans retarder la navigation — et pourquoi le choix entre sendBeacon et fetch keepalive compte pour une collecte sans cookies.

Collecter une vue de page ou un événement est facile tant que la page est active. Le plus difficile est de livrer ce dernier payload au moment où l'utilisateur navigue ailleurs, ferme l'onglet ou met l'application en arrière-plan sur mobile. Se tromper signifie soit perdre des données, soit, pire, retarder la navigation même que l'utilisateur a demandée. Cet article compare les deux options crédibles — navigator.sendBeacon() et fetch() avec keepalive: true — et explique laquelle le tracker de Monoid utilise réellement, et pourquoi, sans cookies, sans stockage ni fingerprinting.

Pourquoi le déchargement est un cas particulier

Lorsqu'un document commence à se décharger, le navigateur est en train de s'arrêter. Le XMLHttpRequest synchrone a longtemps été détourné ici pour bloquer la navigation jusqu'à ce qu'une requête se termine, ce qui nuit à la réactivité. Les navigateurs modernes découragent activement cette pratique, et les requêtes démarrées pendant unload sont fréquemment annulées. La spécification Beacon existe précisément pour résoudre ce problème : elle permet à une page de planifier une requête que le user agent garantit de tenter, de façon asynchrone, sans bloquer le chargement de la page suivante.

L'implication pratique pour l'analytics : ne jamais compter sur un fetch ou un XHR normal déclenché depuis un gestionnaire beforeunload/unload. Il se peut tout simplement qu'il ne quitte jamais la machine.

navigator.sendBeacon

sendBeacon est conçu exactement pour cela. Il met en file d'attente un petit POST et renvoie immédiatement un booléen, indiquant uniquement si la requête a bien été mise en file d'attente — pas si elle a réussi. Le navigateur la transmet ensuite en arrière-plan, même après la disparition de la page.

const ok = navigator.sendBeacon('/collect', payload);

Points forts :

  • Le transfert n'est pas lié à la durée de vie du document.
  • Il est de faible priorité et conçu pour ne pas entrer en concurrence avec la navigation suivante.
  • Il n'y a pas de réponse à traiter, donc rien à attendre.

Contraintes, selon la spécification et les implémentations des navigateurs :

  • POST uniquement. Impossible de définir des méthodes arbitraires.
  • Contrôle limité sur les en-têtes. Le Content-Type est déduit du type du payload (par exemple, un Blob permet de l'influencer, mais on ne peut pas définir librement des en-têtes de requête personnalisés).
  • Les payloads sont comptabilisés dans une limite de données du user agent ; les beacons trop volumineux renvoient false.

Pour une analytics sans cookies, cela suffit généralement — de nombreux trackers s'en tiennent exactement à cela et n'y reviennent jamais. Monoid ne le fait pas, pour une raison précise : sendBeacon ne peut pas exprimer mode: 'cors', et le snippet d'installation de Monoid permet à un site de pointer data-api-url vers une origine différente de celle de la page hôte. Cette contrainte unique tranche la comparaison ci-dessous.

fetch avec keepalive

Le standard Fetch définit un indicateur keepalive qui maintient une requête en vie au-delà de la page qui l'a initiée, offrant la durabilité de sendBeacon avec une API plus riche.

fetch('/collect', {
  method: 'POST',
  keepalive: true,
  headers: { 'Content-Type': 'application/json' },
  body: payload,
});

Points forts :

  • Contrôle total sur la méthode, les en-têtes et le corps.
  • Une véritable Response que l'on peut inspecter (bien que, pendant le déchargement, il ne faille souvent pas l'attendre).

Contraintes :

  • La spécification Fetch limite la taille totale du corps de toutes les requêtes keepalive en cours à 64 Kio par document. Au-delà, le fetch est rejeté. Il s'agit d'un budget par document, donc plusieurs requêtes keepalive concurrentes le partagent.
  • Historiquement, le support de keepalive a accusé du retard par rapport à sendBeacon, et il y a eu des particularités d'implémentation. Testez sur les navigateurs que votre audience utilise réellement.

C'est ce qu'utilise le tracker de Monoid pour chaque requête — /collect, /duration et /event indifféremment :

fetch(endpoint, {
  method: 'POST',
  body: JSON.stringify(payload),
  headers: { 'Content-Type': 'application/json' },
  keepalive: true,
  mode: 'cors',
});

mode: 'cors' est le facteur décisif de la section précédente, pas la taille du payload ni le nombre d'en-têtes — cela vaut la peine de le noter, puisque la plupart des articles présentent ce choix uniquement sous l'angle de la taille.

Que choisir

Une règle pragmatique : utilisez sendBeacon pour la télémétrie de type fire-and-forget où vous n'avez besoin d'aucun retour, et réservez fetch avec keepalive aux cas où vous avez vraiment besoin d'en-têtes personnalisés, d'une requête structurée plus volumineuse, ou d'un contrôle du mode en cross-origin. Gardez les payloads petits dans tous les cas — bien en dessous du budget de 64 Kio du keepalive — parce que les événements analytics doivent être minimaux par conception. La minimisation des données n'est pas qu'une posture de conformité ; c'est ce qui rend possible une livraison fiable au déchargement. Monoid est l'exception que la règle prend déjà en compte : le payload est petit et la réponse n'est jamais lue, exactement le profil pour lequel sendBeacon a été conçu — mais le support de mode: 'cors' l'emporte.

Le bon événement à écouter

Ne vous accrochez pas à unload. Il n'est pas fiable et casse le cache de navigation retour/avant (bfcache). Les recommandations sur le Page Lifecycle préconisent d'écouter visibilitychange et de traiter la transition vers hidden comme le dernier moment fiable pour vider les données. Cela fonctionne de façon cohérente à la fermeture sur desktop, au changement d'onglet et à la mise en arrière-plan sur mobile — des cas où unload ne se déclenche jamais.

document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') flush();
});

Le tracker de Monoid suit ce modèle : un écouteur visibilitychange coexiste avec un repli beforeunload, de sorte qu'un échantillon de durée est signalé qu'un onglet soit mis en arrière-plan sur mobile ou fermé purement et simplement sur desktop.

En quoi cela s'inscrit dans un modèle sans cookies

Comme Monoid ne lit jamais de cookies ni de stockage et ne fait jamais de fingerprinting, chaque requête ne transporte que ce qu'elle révèle déjà — un événement grossier plus les en-têtes que le navigateur envoie de toute façon. Il n'y a aucun identifiant à faire persister, donc nous n'avons jamais besoin d'un échange bidirectionnel pour synchroniser un état. C'est ce qui fait de fetch avec keepalive le choix évident dès que mode: 'cors' élimine sendBeacon : un POST à coup unique dont on n'inspecte jamais la réponse n'est pas une limitation pour nous ; c'est ce qui aligne la fiabilité de la livraison avec le privacy by design.

Sources

Comments

Loading comments…