A Transição de Estado de Visibilidade: Medindo Visualizações de Página Sem Beacons Confiáveis
pagehide, visibilitychange e a Beacon API se comportam de formas diferentes em cada navegador. Veja como registrar o fim de uma sessão de forma confiável, sem cookies nem identificadores persistentes.
Registrar que uma página foi vista é fácil. Registrar quando o usuário realmente saiu — de forma confiável, entre trocas de aba, envio para segundo plano e fechamentos abruptos — é um dos problemas mais espinhosos de web analytics. Isso importa porque métricas de engajamento (tempo de permanência, rejeição, profundidade de rolagem) dependem de capturar um sinal limpo de fim de sessão. Se você erra, ou perde dados ou os infla.
O Monoid resolve isso sem cookies, localStorage ou qualquer identificador persistente. Essa restrição na verdade simplifica as coisas: nunca precisamos reconciliar uma sessão entre carregamentos de página, então tudo o que importa é emitir um único beacon final e preciso por ciclo de vida da página. Veja como as primitivas do navegador se comportam — e onde estão as armadilhas.
Por que unload é o evento errado
A abordagem histórica era escutar unload ou beforeunload e disparar um XHR síncrono. Hoje isso é ativamente prejudicial. O evento unload não é confiável no mobile: quando o usuário manda a aba para segundo plano e o sistema operacional a recupera, unload frequentemente nunca dispara. Pior, registrar um handler de unload desqualifica a página do cache de avanço/retrocesso (bfcache) em vários navegadores, prejudicando o desempenho de navegação.
A orientação sobre a Page Lifecycle API no web.dev é explícita: trate a transição de visibilitychange para hidden como o último evento confiável que você vai observar. Não conte com unload ou pagehide disparando no mobile.
A dupla visibilitychange + pagehide
O padrão robusto é escutar visibilitychange e verificar document.visibilityState === 'hidden', com pagehide como sinal secundário. Sempre que a página transiciona para oculta, você descarrega o que precisa enviar. A documentação da Page Visibility API no MDN confirma que visibilitychange dispara quando uma aba vai para segundo plano ou o navegador é minimizado — a aproximação mais próxima de "o usuário parou de olhar".
A complicação: visibilitychange para oculta pode disparar várias vezes em uma sessão (sair da aba, voltar, sair de novo). Então você precisa tornar o envio idempotente, ou aplicar debounce, ou enviar apenas o delta desde o último envio. Como o Monoid não guarda nenhum estado por usuário, enviamos um único payload autocontido e deixamos a edge deduplicar no escopo da requisição.
Enviando dados de uma página moribunda: navigator.sendBeacon
Você não pode disparar um fetch assíncrono durante o visibilitychange e esperar que ele conclua — o navegador pode destruir a página antes. É exatamente para isso que existe a especificação da Beacon API. navigator.sendBeacon(url, data) enfileira um pequeno POST que o navegador garante tentar mesmo depois que o documento se foi, sem bloquear a thread principal nem atrasar a navegação.
O Beacon tem limites que vale respeitar:
- Tamanho do payload. A especificação permite que o user agent rejeite beacons acima de um limite definido pela implementação (comumente 64 KB). Mantenha payloads minúsculos — os do Monoid têm algumas centenas de bytes.
- Método. Beacons são sempre POST. Se seu endpoint na edge espera GET, adapte-o.
- Sem resposta. Você não consegue ler nada de volta. Dispare e esqueça.
Uma alternativa moderna é fetch(url, { keepalive: true }), que o padrão Fetch define como permitindo que uma requisição sobreviva ao documento. keepalive te dá cabeçalhos e métodos que o Beacon não tem, mas compartilha o mesmo teto de tamanho. Use Beacon primeiro e caia para fetch com keepalive.
Uma implementação mínima e sem cookies
let sent = false;
function flush() {
if (sent) return;
sent = true;
const payload = JSON.stringify({
path: location.pathname,
// sem identificadores, sem cookies, sem 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);
Note o que está ausente: nenhum listener de unload, então a elegibilidade ao bfcache é preservada. Nenhum ID armazenado entre carregamentos. O valor de visibleMs usa performance.now(), um relógio monotônico que não precisa de timestamp de relógio de parede nem de estado persistente.
Por que isso combina com analytics privacy-first
Como o Monoid nunca costura sessões, cada beacon é uma observação completa e descartável. Não há identificador entre páginas a proteger, então um beacon perdido custa a você um ponto de dado, não um perfil de usuário corrompido. O padrão visibilitychange mais Beacon oferece medição precisa de permanência mantendo a página rápida e amigável ao bfcache — o que por si só melhora os Core Web Vitals que você talvez esteja tentando medir.
Meça a saída, não a pessoa. O navegador já te dá tudo de que você precisa.
Comments
Loading comments…