Volver al blog

navigator.sendBeacon vs fetch keepalive: Entrega Fiable de Analítica Sin Bloquear la Navegación

Cómo enviar payloads de analítica que sobreviven al descargue de la página sin retrasar la navegación — y por qué la elección entre sendBeacon y fetch keepalive importa para la recolección sin cookies.

Recolectar una vista de página o un evento es fácil mientras la página está viva. Lo difícil es entregar ese último payload justo cuando el usuario navega a otro lugar, cierra la pestaña o pasa la app a segundo plano en el móvil. Si te equivocas, pierdes datos o, peor aún, retrasas la propia navegación que el usuario pidió. Este artículo compara las dos opciones viables — navigator.sendBeacon() y fetch() con keepalive: true — y explica cuál usa realmente el tracker de Monoid, y por qué, sin cookies, almacenamiento ni fingerprinting.

Por qué el descargue es un caso especial

Cuando un documento comienza a descargarse, el navegador está cerrando el proceso. El XMLHttpRequest síncrono se usó históricamente de forma abusiva aquí para bloquear la navegación hasta que una petición terminara, lo que perjudica la capacidad de respuesta. Los navegadores modernos desalientan activamente esto, y las peticiones iniciadas durante el unload se cancelan con frecuencia. La especificación Beacon existe precisamente para resolver esto: permite que una página programe una petición que el user agent garantiza intentar, de forma asíncrona, sin bloquear la carga de la siguiente página.

La implicación práctica para la analítica: nunca confíes en un fetch o XHR normal disparado desde un manejador de beforeunload/unload. Puede que simplemente nunca salga de la máquina.

navigator.sendBeacon

sendBeacon está diseñado justo para esto. Encola un POST pequeño y devuelve un booleano de inmediato, indicando solo si la petición se encoló con éxito — no si tuvo éxito. El navegador la transmite después en segundo plano, incluso después de que la página haya desaparecido.

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

Puntos fuertes:

  • La transferencia no está atada al tiempo de vida del documento.
  • Es de baja prioridad y está diseñada para no competir con la siguiente navegación.
  • No hay respuesta que gestionar, así que no hay nada que esperar.

Restricciones, según la especificación y las implementaciones de los navegadores:

  • Solo POST. No puedes establecer métodos arbitrarios.
  • Control limitado sobre las cabeceras. El Content-Type se infiere a partir del tipo del payload (por ejemplo, un Blob te permite influir en él, pero no puedes establecer libremente cabeceras de petición personalizadas).
  • Los payloads cuentan contra un límite de datos del user agent; los beacons demasiado grandes devuelven false.

Para analítica sin cookies, esto suele ser suficiente — muchos trackers implementan exactamente esto y nunca miran atrás. Monoid no lo hace, por un motivo concreto: sendBeacon no puede expresar mode: 'cors', y el snippet de instalación de Monoid permite que un sitio apunte data-api-url a un origen distinto al de la página anfitriona. Esa única restricción decide la comparación de abajo.

fetch con keepalive

El estándar Fetch define una marca keepalive que mantiene viva una petición más allá de la página que la inició, dándote la durabilidad de sendBeacon con una API más completa.

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

Puntos fuertes:

  • Control total sobre método, cabeceras y cuerpo.
  • Una Response real que puedes inspeccionar (aunque durante el descargue, a menudo no deberías esperarla).

Restricciones:

  • La especificación Fetch limita el tamaño total del cuerpo de todas las peticiones keepalive en curso a 64 KiB por documento. Si lo superas, el fetch se rechaza. Es un presupuesto por documento, así que varias peticiones keepalive concurrentes lo comparten.
  • Históricamente, el soporte de keepalive ha ido por detrás del de sendBeacon, y ha habido peculiaridades de implementación. Pruébalo en los navegadores que realmente usa tu audiencia.

Esto es lo que usa el tracker de Monoid para cada petición — /collect, /duration y /event por igual:

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

El mode: 'cors' es el factor decisivo de la sección anterior, no el tamaño del payload ni el número de cabeceras — vale la pena recordarlo, porque la mayoría de los artículos plantean la elección únicamente en torno al tamaño.

Cuál elegir

Una regla pragmática: usa sendBeacon para telemetría de disparar-y-olvidar donde no necesitas ninguna respuesta, y recurre a fetch con keepalive solo cuando realmente necesites cabeceras personalizadas, una petición estructurada más grande, o control sobre el mode entre orígenes. Mantén los payloads pequeños de todos modos — muy por debajo del presupuesto de 64 KiB del keepalive — porque los eventos de analítica deben ser mínimos por diseño. La minimización de datos no es solo una postura de cumplimiento; es lo que hace posible una entrega fiable durante el descargue. Monoid es la excepción que la regla ya contempla: el payload es pequeño y la respuesta nunca se lee, exactamente el perfil para el que se creó sendBeacon — pero el soporte de mode: 'cors' se impone.

El evento correcto para escuchar

No uses unload. No es fiable y rompe la caché de avance/retroceso (bfcache). La guía de Page Lifecycle recomienda escuchar visibilitychange y tratar la transición a hidden como tu último momento fiable para volcar los datos. Esto funciona de forma consistente en el cierre en escritorio, el cambio de pestaña y el paso a segundo plano en móvil — casos en los que unload nunca se dispara.

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

El tracker de Monoid sigue este patrón: un listener de visibilitychange convive con un fallback de beforeunload, de modo que se reporta una muestra de duración tanto si una pestaña pasa a segundo plano en móvil como si se cierra por completo en escritorio.

Cómo encaja esto en un modelo sin cookies

Como Monoid nunca lee cookies ni almacenamiento y nunca hace fingerprinting, cada petición lleva solo lo que ya revela por sí misma — un evento genérico más las cabeceras que el navegador envía de todos modos. No hay ningún identificador que persistir, así que nunca necesitamos un intercambio bidireccional para sincronizar estado. Eso es lo que convierte a fetch con keepalive en la elección fácil en cuanto mode: 'cors' deja fuera a sendBeacon: un POST de un solo disparo cuya respuesta nunca inspeccionamos no es una limitación para nosotros; es lo que alinea la fiabilidad de la entrega con la privacidad desde el diseño.

Sources

Comments

Loading comments…