La Transición del Estado de Visibilidad: Medir Vistas de Página Sin Beacons Fiables
pagehide, visibilitychange y la Beacon API se comportan de forma distinta en cada navegador. Así se registra el final de una sesión de manera fiable, sin cookies ni identificadores persistentes.
Registrar que una página se ha visto es fácil. Registrar cuándo se marchó realmente el usuario —de forma fiable, entre cambios de pestaña, paso a segundo plano y cierres bruscos— es uno de los problemas más espinosos de la analítica web. Importa porque las métricas de interacción (tiempo de permanencia, rebote, profundidad de scroll) dependen de capturar una señal limpia de fin de sesión. Si te equivocas, o pierdes datos o los inflas.
Monoid resuelve esto sin cookies, localStorage ni ningún identificador persistente. Esa restricción en realidad simplifica las cosas: nunca necesitamos reconciliar una sesión entre cargas de página, así que lo único que importa es emitir un único beacon final y preciso por ciclo de vida de la página. Esto es cómo se comportan las primitivas del navegador y dónde están las trampas.
Por qué unload es el evento equivocado
El enfoque histórico consistía en escuchar unload o beforeunload y lanzar un XHR síncrono. Hoy eso es activamente dañino. El evento unload no es fiable en móvil: cuando el usuario manda una pestaña a segundo plano y el sistema operativo la reclama, unload a menudo no llega a dispararse. Peor aún, registrar un manejador de unload descalifica la página de la caché de avance/retroceso (bfcache) en varios navegadores, perjudicando el rendimiento de navegación.
La guía de la Page Lifecycle API de web.dev es explícita: trata la transición de visibilitychange a hidden como el último evento fiable que vas a observar. No cuentes con que unload o pagehide se disparen en móvil.
El emparejamiento visibilitychange + pagehide
El patrón robusto es escuchar visibilitychange y comprobar document.visibilityState === 'hidden', más pagehide como señal secundaria. Cada vez que la página pasa a oculta, vuelcas lo que necesites enviar. La documentación de MDN sobre la Page Visibility API confirma que visibilitychange se dispara cuando una pestaña pasa a segundo plano o el navegador se minimiza: la aproximación más cercana a "el usuario dejó de mirar".
La complicación: visibilitychange a oculta puede dispararse varias veces en una sesión (cambiar de pestaña, volver, cambiar otra vez). Así que debes hacer tu volcado idempotente, o aplicarle debounce, o enviar solo el delta desde el último volcado. Como Monoid no guarda estado por usuario, enviamos una única carga autocontenida y dejamos que el edge deduplique en el ámbito de la petición.
Enviar datos desde una página moribunda: navigator.sendBeacon
No puedes lanzar un fetch asíncrono durante visibilitychange y esperar que se complete: el navegador puede desmontar la página antes. Para esto existe la especificación de la Beacon API. navigator.sendBeacon(url, data) encola un POST pequeño que el navegador garantiza intentar incluso después de que el documento haya desaparecido, sin bloquear el hilo principal ni retrasar la navegación.
Beacon tiene límites que conviene respetar:
- Tamaño de la carga. La especificación permite al agente de usuario rechazar beacons por encima de un límite definido por la implementación (habitualmente 64 KB). Mantén las cargas diminutas: las de Monoid son de unos pocos cientos de bytes.
- Método. Los beacons siempre son POST. Si tu endpoint en el edge espera GET, adáptalo.
- Sin respuesta. No puedes leer nada de vuelta. Dispara y olvida.
Una alternativa moderna es fetch(url, { keepalive: true }), que el estándar Fetch define como permitir que una petición sobreviva al documento. keepalive te da cabeceras y métodos de los que Beacon carece, pero comparte el mismo techo de tamaño. Usa Beacon primero y recurre a fetch con keepalive.
Una implementación mínima y sin cookies
let sent = false;
function flush() {
if (sent) return;
sent = true;
const payload = JSON.stringify({
path: location.pathname,
// sin identificadores, sin cookies, sin fingerprint
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);
Fíjate en lo que falta: ningún listener de unload, así que se preserva la elegibilidad para bfcache. Ningún ID almacenado entre cargas. La cifra de visibleMs usa performance.now(), un reloj monotónico que no necesita marca de tiempo de reloj de pared ni estado persistente.
Por qué esto encaja con la analítica privacy-first
Como Monoid nunca cose sesiones entre sí, cada beacon es una observación completa y desechable. No hay identificador entre páginas que proteger, así que un beacon perdido te cuesta un punto de datos, no un perfil de usuario corrompido. El patrón visibilitychange más Beacon te da una medición precisa de la permanencia manteniendo la página rápida y compatible con bfcache, lo que a su vez mejora las Core Web Vitals que quizá estés intentando medir.
Mide la marcha, no a la persona. El navegador ya te da todo lo que necesitas.
Comments
Loading comments…