Voltar ao blog

navigator.sendBeacon vs fetch keepalive: Entrega Confiável de Analytics Sem Bloquear a Navegação

Como enviar payloads de analytics que sobrevivem ao descarregamento da página sem travar a navegação — e por que a escolha entre sendBeacon e fetch keepalive importa para coleta sem cookies.

Coletar uma visualização de página ou um evento é fácil enquanto a página está viva. A parte difícil é entregar esse último payload no momento em que o usuário navega para outro lugar, fecha a aba ou coloca o app em segundo plano no celular. Errar isso significa perder dados ou, pior, atrasar a própria navegação que o usuário pediu. Este post compara as duas opções viáveis — navigator.sendBeacon() e fetch() com keepalive: true — e explica qual delas o tracker do Monoid realmente usa, e por quê, sem cookies, armazenamento ou fingerprinting.

Por que o descarregamento é um caso especial

Quando um documento começa a ser descarregado, o navegador está se desligando. XMLHttpRequest síncrono foi historicamente abusado aqui para bloquear a navegação até uma requisição terminar, o que prejudica a responsividade. Navegadores modernos desencorajam ativamente isso, e requisições iniciadas durante o unload são frequentemente canceladas. A especificação Beacon existe justamente para resolver isso: ela permite que uma página agende uma requisição que o user agent garante tentar, de forma assíncrona, sem bloquear o carregamento da próxima página.

A implicação prática para analytics: nunca confie em um fetch ou XHR normal disparado a partir de um handler de beforeunload/unload. Ele pode simplesmente nunca sair da máquina.

navigator.sendBeacon

sendBeacon foi feito exatamente para isso. Ele enfileira um pequeno POST e retorna um booleano imediatamente, dizendo apenas se a requisição foi enfileirada com sucesso — não se ela teve êxito. O navegador então a transmite em segundo plano, mesmo depois que a página já se foi.

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

Pontos fortes:

  • A transferência não está atrelada ao tempo de vida do documento.
  • É de baixa prioridade e projetada para não competir com a próxima navegação.
  • Não há resposta para tratar, então não há nada para aguardar.

Restrições, segundo a especificação e as implementações dos navegadores:

  • Apenas POST. Você não pode definir métodos arbitrários.
  • Controle limitado sobre cabeçalhos. O Content-Type é inferido a partir do tipo do payload (por exemplo, um Blob permite influenciá-lo, mas você não pode definir livremente cabeçalhos de requisição personalizados).
  • Payloads contam contra um limite de dados do user agent; beacons grandes demais retornam false.

Para analytics sem cookies, isso costuma ser suficiente — vários trackers usam exatamente isso e nunca olham para trás. O Monoid não usa, por um motivo específico: sendBeacon não consegue expressar mode: 'cors', e o snippet de instalação do Monoid permite que um site aponte data-api-url para uma origem diferente da página host. Essa única restrição decide a comparação abaixo.

fetch com keepalive

O padrão Fetch define uma flag keepalive que mantém uma requisição viva além da página que a iniciou, dando a você a durabilidade do sendBeacon com uma API mais rica.

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

Pontos fortes:

  • Controle total sobre método, cabeçalhos e corpo.
  • Uma Response real que você pode inspecionar (embora durante o descarregamento, muitas vezes você não deva aguardá-la).

Restrições:

  • A especificação Fetch limita o tamanho total do corpo de todas as requisições keepalive em andamento a 64 KiB por documento. Ultrapasse isso e o fetch é rejeitado. É um orçamento por documento, então várias requisições keepalive concorrentes o compartilham.
  • Historicamente, o suporte a keepalive ficou atrás do sendBeacon, e houve peculiaridades de implementação. Teste nos navegadores que seu público realmente usa.

É isso que o tracker do Monoid usa para toda requisição — /collect, /duration e /event igualmente:

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

O mode: 'cors' é o fator decisivo da seção anterior, não o tamanho do payload nem a quantidade de cabeçalhos — vale lembrar, já que a maioria dos textos sobre o tema enquadra a escolha puramente em torno do tamanho.

Qual escolher

Uma regra pragmática: use sendBeacon para telemetria de disparar-e-esquecer, onde você não precisa de nada de volta, e recorra ao fetch com keepalive apenas quando você genuinamente precisa de cabeçalhos personalizados, uma requisição estruturada maior, ou controle sobre o mode entre origens. Mantenha os payloads pequenos de qualquer forma — bem abaixo do orçamento de 64 KiB do keepalive — porque eventos de analytics devem ser mínimos por design. Minimização de dados não é apenas uma postura de conformidade; é o que torna possível uma entrega confiável no descarregamento. O Monoid é a exceção que a regra já prevê: o payload é pequeno e a resposta nunca é lida, exatamente o perfil para o qual o sendBeacon foi criado — mas o suporte a mode: 'cors' prevalece.

O evento certo para escutar

Não use o unload. Ele não é confiável e quebra o cache de avanço/retrocesso (bfcache). A orientação do Page Lifecycle recomenda escutar visibilitychange e tratar a transição para hidden como seu último momento confiável para descarregar dados. Isso funciona de forma consistente entre o fechamento no desktop, a troca de aba e o envio para segundo plano no celular — casos em que o unload nunca dispara.

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

O tracker do Monoid segue esse padrão: um listener de visibilitychange fica ao lado de um fallback de beforeunload, então uma amostra de duração é reportada seja quando uma aba é enviada para segundo plano no celular, seja quando é fechada de vez no desktop.

Como isso se encaixa em um modelo sem cookies

Como o Monoid nunca lê cookies ou armazenamento e nunca faz fingerprinting, cada requisição carrega apenas o que já revela por si só — um evento genérico mais os cabeçalhos que o navegador envia de qualquer forma. Não há identificador para persistir, então nunca precisamos de uma troca bidirecional para sincronizar estado. É isso que torna o fetch com keepalive a escolha fácil assim que o mode: 'cors' tira o sendBeacon da mesa: um POST de disparo único cuja resposta nunca inspecionamos não é uma limitação para nós; é o que alinha a confiabilidade da entrega com privacidade desde a concepção.

Sources

Comments

Loading comments…