La Transizione di Stato della Visibilità: Misurare le Visualizzazioni di Pagina Senza Beacon Affidabili
pagehide, visibilitychange e la Beacon API si comportano in modo diverso da browser a browser. Ecco come registrare la fine di una sessione in modo affidabile, senza cookie né identificatori persistenti.
Registrare che una pagina è stata vista è facile. Registrare quando l'utente se n'è davvero andato — in modo affidabile, tra cambi di scheda, passaggi in background e chiusure brusche — è uno dei problemi più insidiosi dell'analytics web. Conta, perché le metriche di coinvolgimento (tempo di permanenza, rimbalzo, profondità di scroll) dipendono dal catturare un segnale pulito di fine sessione. Se sbagli, o perdi dati o li gonfi.
Monoid risolve la cosa senza cookie, localStorage o qualsiasi identificatore persistente. Questo vincolo in realtà semplifica: non dobbiamo mai riconciliare una sessione tra più caricamenti di pagina, quindi l'unica cosa che conta è emettere un solo beacon finale e accurato per ciclo di vita della pagina. Ecco come si comportano le primitive del browser e dove sono le trappole.
Perché unload è l'evento sbagliato
L'approccio storico era mettersi in ascolto di unload o beforeunload e sparare una XHR sincrona. Oggi è attivamente dannoso. L'evento unload non è affidabile su mobile: quando l'utente manda una scheda in background e il sistema operativo la reclama, unload spesso non scatta mai. Peggio, registrare un handler unload squalifica la pagina dalla back/forward cache (bfcache) in diversi browser, penalizzando le prestazioni di navigazione.
La guida alla Page Lifecycle API di web.dev è esplicita: tratta il passaggio di visibilitychange a hidden come l'ultimo evento affidabile che osserverai. Non contare sul fatto che unload o pagehide scattino su mobile.
L'accoppiata visibilitychange + pagehide
Il pattern robusto è ascoltare visibilitychange e controllare document.visibilityState === 'hidden', più pagehide come segnale secondario. Ogni volta che la pagina passa a nascosta, invii quello che devi inviare. La documentazione MDN della Page Visibility API conferma che visibilitychange scatta quando una scheda va in background o il browser viene ridotto a icona — l'approssimazione più vicina a "l'utente ha smesso di guardare".
La complicazione: il passaggio a nascosta può scattare più volte in una sessione (esci dalla scheda, torni, esci di nuovo). Devi quindi rendere idempotente l'invio, applicare un debounce, oppure spedire solo il delta dall'ultimo invio. Poiché Monoid non conserva alcuno stato per utente, inviamo un singolo payload autocontenuto e lasciamo che l'edge deduplichi nell'ambito della richiesta.
Inviare dati da una pagina morente: navigator.sendBeacon
Non puoi lanciare una fetch asincrona durante visibilitychange e aspettarti che si completi: il browser potrebbe smontare la pagina prima. È esattamente per questo che esiste la specifica della Beacon API. navigator.sendBeacon(url, data) accoda una piccola POST che il browser garantisce di tentare anche dopo che il documento è sparito, senza bloccare il thread principale né ritardare la navigazione.
Beacon ha limiti che vale la pena rispettare:
- Dimensione del payload. La specifica consente allo user agent di rifiutare beacon oltre un limite definito dall'implementazione (di solito 64 KB). Tieni i payload minuscoli: quelli di Monoid sono di poche centinaia di byte.
- Metodo. I beacon sono sempre POST. Se il tuo endpoint sull'edge si aspetta una GET, adattalo.
- Nessuna risposta. Non puoi leggere nulla di ritorno. Spedisci e dimentica.
Un fallback moderno è fetch(url, { keepalive: true }), che lo standard Fetch definisce come il permesso per una richiesta di sopravvivere al documento. keepalive ti dà header e metodi che a Beacon mancano, ma condivide lo stesso tetto di dimensione. Usa prima Beacon e ripiega su fetch con keepalive.
Un'implementazione minima e senza cookie
let sent = false;
function flush() {
if (sent) return;
sent = true;
const payload = JSON.stringify({
path: location.pathname,
// nessun identificatore, nessun cookie, nessun 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);
Nota ciò che è assente: nessun listener su unload, così l'idoneità alla bfcache è preservata. Nessun ID memorizzato tra un caricamento e l'altro. Il valore visibleMs usa performance.now(), un orologio monotono che non richiede né un timestamp di orologio da parete né stato persistente.
Perché questo si sposa con l'analytics privacy-first
Poiché Monoid non ricuce mai insieme le sessioni, ogni beacon è un'osservazione completa e usa-e-getta. Non c'è alcun identificatore tra pagine da proteggere, quindi un beacon perso ti costa un dato, non un profilo utente corrotto. Il pattern visibilitychange più Beacon ti dà una misura accurata della permanenza mantenendo la pagina veloce e compatibile con la bfcache — cosa che di per sé migliora i Core Web Vitals che magari stai cercando di misurare.
Misura l'uscita, non la persona. Il browser ti dà già tutto ciò che ti serve.
Comments
Loading comments…