Retour au blog

La Transition d'État de Visibilité : Mesurer les Pages Vues Sans Beacons Fiables

pagehide, visibilitychange et la Beacon API se comportent différemment selon les navigateurs. Voici comment enregistrer la fin d'une session de façon fiable, sans cookies ni identifiants persistants.

Enregistrer qu'une page a été vue est facile. Enregistrer le moment où l'utilisateur est réellement parti — de façon fiable, à travers les changements d'onglet, les mises en arrière-plan et les fermetures brutales — est l'un des problèmes les plus épineux de l'analytics web. Cela compte parce que les métriques d'engagement (temps passé, rebond, profondeur de défilement) dépendent de la capture d'un signal propre de fin de session. Si vous vous trompez, vous perdez des données ou vous les gonflez.

Monoid résout cela sans cookies, sans localStorage et sans aucun identifiant persistant. Cette contrainte simplifie en réalité les choses : nous n'avons jamais besoin de réconcilier une session entre deux chargements de page, donc tout ce qui compte est d'émettre un seul beacon final exact par cycle de vie de page. Voici comment se comportent les primitives du navigateur, et où se trouvent les pièges.

Pourquoi unload est le mauvais événement

L'approche historique consistait à écouter unload ou beforeunload et à déclencher un XHR synchrone. C'est aujourd'hui activement nuisible. L'événement unload n'est pas fiable sur mobile : lorsqu'un onglet passe en arrière-plan et que l'OS le récupère, unload ne se déclenche souvent jamais. Pire, enregistrer un gestionnaire unload disqualifie la page du cache avant/arrière (bfcache) dans plusieurs navigateurs, ce qui pénalise les performances de navigation.

Le guide de la Page Lifecycle API sur web.dev est explicite : traitez le passage de visibilitychange à hidden comme le dernier événement fiable que vous observerez. Ne comptez pas sur unload ni pagehide sur mobile.

Le duo visibilitychange + pagehide

Le motif robuste consiste à écouter visibilitychange et à vérifier document.visibilityState === 'hidden', avec pagehide comme signal secondaire. Dès que la page passe à l'état masqué, vous videz ce que vous avez à envoyer. La documentation MDN de la Page Visibility API confirme que visibilitychange se déclenche quand un onglet passe en arrière-plan ou que le navigateur est réduit — l'approximation la plus proche de « l'utilisateur a cessé de regarder ».

La complication : le passage à masqué peut se déclencher plusieurs fois dans une session (quitter l'onglet, revenir, repartir). Vous devez donc rendre votre envoi idempotent, le debouncer, ou n'envoyer que le delta depuis le dernier envoi. Comme Monoid ne conserve aucun état par utilisateur, nous envoyons une charge utile unique et autonome, et laissons l'edge dédupliquer à l'échelle de la requête.

Envoyer des données depuis une page mourante : navigator.sendBeacon

Vous ne pouvez pas déclencher un fetch asynchrone pendant visibilitychange et espérer qu'il aboutisse — le navigateur peut démonter la page avant. C'est précisément la raison d'être de la spécification de la Beacon API. navigator.sendBeacon(url, data) met en file un petit POST que le navigateur garantit de tenter même après la disparition du document, sans bloquer le thread principal ni retarder la navigation.

Beacon a des limites qu'il vaut mieux respecter :

  • Taille de la charge utile. La spécification autorise l'agent utilisateur à rejeter les beacons dépassant une limite définie par l'implémentation (souvent 64 Ko). Gardez des charges minuscules — celles de Monoid font quelques centaines d'octets.
  • Méthode. Les beacons sont toujours en POST. Si votre point de terminaison edge attend un GET, adaptez-le.
  • Pas de réponse. Vous ne pouvez rien relire. Envoyez et oubliez.

Une solution de repli moderne est fetch(url, { keepalive: true }), que le standard Fetch définit comme autorisant une requête à survivre au document. keepalive vous donne des en-têtes et des méthodes qui manquent à Beacon, mais partage le même plafond de taille. Utilisez Beacon d'abord, repliez-vous sur fetch avec keepalive.

Une implémentation minimale et sans cookies

let sent = false;
function flush() {
  if (sent) return;
  sent = true;
  const payload = JSON.stringify({
    path: location.pathname,
    // aucun identifiant, aucun cookie, aucune empreinte
    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);

Notez ce qui est absent : aucun écouteur unload, donc l'éligibilité au bfcache est préservée. Aucun identifiant stocké entre les chargements. La valeur visibleMs utilise performance.now(), une horloge monotone qui n'exige ni horodatage d'horloge murale ni état persistant.

Pourquoi cela convient à l'analytics privacy-first

Parce que Monoid ne recoud jamais les sessions entre elles, chaque beacon est une observation complète et jetable. Il n'y a aucun identifiant inter-pages à protéger, donc un beacon perdu vous coûte un point de donnée, pas un profil utilisateur corrompu. Le motif visibilitychange plus Beacon offre une mesure exacte du temps passé tout en gardant la page rapide et compatible bfcache — ce qui améliore en soi les Core Web Vitals que vous cherchez peut-être à mesurer.

Mesurez le départ, pas la personne. Le navigateur vous donne déjà tout ce dont vous avez besoin.

Sources

Comments

Loading comments…