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 campione finale accurato per ciclo di vita della pagina. Ecco come si comportano le primitive del browser, dove sono le trappole, e quale di esse usa davvero il tracker di Monoid.
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 azzerare il tuo orologio così che ogni scatto riporti un segmento nuovo e senza sovrapposizioni invece di un totale crescente. Monoid segue quest'ultimo approccio: il timer dietro il campione di permanenza si azzera a ogni transizione di visibilitychange, quindi un secondo evento hidden più avanti nello stesso ciclo di vita della pagina riporta solo il tempo trascorso dall'ultimo invio. Monoid ascolta visibilitychange (accanto a un fallback beforeunload già esistente) ma non aggiunge un listener pagehide separato — i due si sovrappongono abbastanza sui casi che contano perché un secondo listener reintroduca solo il problema del doppio conteggio, senza coprire un terreno significativamente maggiore.
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. La saggezza comune dice di usare prima Beacon e ripiegare su fetch con keepalive — Monoid in realtà inverte l'ordine, per le ragioni che spiega la sezione successiva.
Un'implementazione minima e senza cookie
Ecco un pattern di riferimento che combina tutto quanto sopra — prima sendBeacon, fetch keepalive come suo fallback, attivato sia da visibilitychange sia da pagehide, con una guardia perché uno scatto ripetuto non invii due volte:
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.
Il tracker di Monoid segue un percorso simile ma distinto: solo fetch keepalive (niente sendBeacon — non può esprimere il mode: 'cors' di cui ha bisogno un'installazione cross-origin), nessuna guardia sent, e un timer che si azzera a ogni transizione di visibilità invece di scattare una volta e tacere (il filtro di consenso e Do Not Track è omesso sotto per chiarezza):
function dur() {
fetch('/duration', {
method: 'POST',
body: JSON.stringify({ site_id: siteId, duration_ms: Date.now() - t }),
headers: { 'Content-Type': 'application/json' },
keepalive: true,
mode: 'cors',
});
}
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') dur();
t = Date.now();
});
window.addEventListener('beforeunload', dur);
Azzerare t a ogni transizione fa lo stesso lavoro della guardia sent sopra, senza bisogno di un flag: ogni chiamata riporta solo il segmento trascorso dall'ultimo azzeramento dell'orologio, quindi uno scatto di visibilitychange seguito da un beforeunload per la stessa chiusura di scheda riporta un segmento reale e uno quasi-zero innocuo, invece di un doppio conteggio.
Perché questo si sposa con l'analytics privacy-first
Poiché Monoid non ricuce mai insieme le sessioni, ogni richiesta è un'osservazione completa e usa-e-getta. Non c'è alcun identificatore tra pagine da proteggere, quindi un campione perso o duplicato ti costa un dato, non un profilo utente corrotto. Il pattern visibilitychange più beforeunload che Monoid distribuisce davvero 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…