Casi d'uso del metodo sendBeacon() in JavaScript
Il metodo navigator.sendBeacon() è una delle API più utili e meno conosciute del browser moderno. Introdotto per risolvere un problema specifico legato all'invio di dati durante la chiusura di una pagina, questo metodo offre un approccio asincrono e affidabile per trasmettere piccole quantità di informazioni al server senza bloccare la navigazione dell'utente. In questo articolo analizzeremo in dettaglio i principali casi d'uso, evidenziandone i vantaggi rispetto alle tradizionali richieste HTTP basate su fetch o XMLHttpRequest.
Cos'è sendBeacon() e perché è stato introdotto
Il problema che sendBeacon() risolve è ben noto a chi sviluppa applicazioni web: quando l'utente chiude una scheda, naviga verso un altro sito o ricarica la pagina, eventuali richieste HTTP in corso vengono tipicamente annullate dal browser. Questo comportamento rende inaffidabili le tradizionali tecniche di invio dati durante eventi come unload o beforeunload. Storicamente, gli sviluppatori ricorrevano a richieste sincrone con XMLHttpRequest per garantire che i dati arrivassero al server, ma questa pratica blocca il thread principale e degrada l'esperienza utente.
Il metodo sendBeacon() accetta due parametri: l'URL di destinazione e il payload da inviare. Restituisce un valore booleano che indica se la richiesta è stata accodata correttamente dal browser. La caratteristica fondamentale è che il browser garantisce l'invio della richiesta anche dopo la chiusura della pagina, senza bloccare la navigazione.
// Sintassi di base del metodo sendBeacon
const url = "https://example.com/analytics";
const data = JSON.stringify({ event: "page_close", timestamp: Date.now() });
const success = navigator.sendBeacon(url, data);
if (success) {
console.log("Richiesta accodata correttamente");
} else {
console.error("Impossibile accodare la richiesta");
}
Tracciamento di eventi analitici
Il primo e più diffuso caso d'uso di sendBeacon() è il tracciamento di eventi analitici. Le piattaforme di analytics moderne, come Google Analytics, utilizzano internamente questo metodo per inviare dati di telemetria al server senza compromettere le performance percepite dall'utente. Quando vogliamo registrare metriche come il tempo trascorso su una pagina, lo scroll depth o le interazioni dell'utente prima dell'abbandono, sendBeacon() è la scelta ideale.
Consideriamo un esempio concreto in cui vogliamo inviare al server le statistiche di sessione quando l'utente lascia la pagina. Utilizziamo l'evento visibilitychange che è considerato più affidabile rispetto a beforeunload sui browser moderni, soprattutto su dispositivi mobili.
// Tracker di sessione utente
const sessionTracker = {
startTime: Date.now(),
pageViews: 0,
interactions: 0,
trackInteraction() {
this.interactions++;
},
sendSessionData() {
const sessionData = {
duration: Date.now() - this.startTime,
pageViews: this.pageViews,
interactions: this.interactions,
url: window.location.href,
referrer: document.referrer
};
const payload = JSON.stringify(sessionData);
const endpoint = "https://api.example.com/analytics/session";
return navigator.sendBeacon(endpoint, payload);
}
};
// Invio dei dati quando la pagina diventa nascosta
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
sessionTracker.sendSessionData();
}
});
L'uso di visibilitychange con stato hidden è particolarmente importante perché copre sia il caso di chiusura della scheda sia quello in cui l'utente passa a un'altra applicazione su mobile, dove l'evento unload potrebbe non essere mai attivato.
Logging di errori e crash reporting
Un altro scenario in cui sendBeacon() brilla è il reporting di errori applicativi. Quando si verifica un'eccezione non gestita, è cruciale inviare le informazioni diagnostiche al server prima che l'utente abbandoni la pagina o che lo stato dell'applicazione si corrompa ulteriormente. Combinando il listener globale error con sendBeacon(), possiamo costruire un sistema di error tracking robusto.
// Sistema di error reporting
class ErrorReporter {
constructor(endpoint) {
this.endpoint = endpoint;
this.queue = [];
this.setupListeners();
}
setupListeners() {
window.addEventListener("error", (event) => {
this.captureError({
type: "javascript_error",
message: event.message,
filename: event.filename,
line: event.lineno,
column: event.colno,
stack: event.error ? event.error.stack : null,
timestamp: Date.now()
});
});
window.addEventListener("unhandledrejection", (event) => {
this.captureError({
type: "promise_rejection",
message: event.reason ? event.reason.toString() : "Unknown rejection",
timestamp: Date.now()
});
});
}
captureError(errorData) {
// Aggiunta di metadati di contesto
const enrichedData = {
...errorData,
url: window.location.href,
userAgent: navigator.userAgent,
viewport: {
width: window.innerWidth,
height: window.innerHeight
}
};
const blob = new Blob(
[JSON.stringify(enrichedData)],
{ type: "application/json" }
);
navigator.sendBeacon(this.endpoint, blob);
}
}
const reporter = new ErrorReporter("https://api.example.com/errors");
Notiamo l'uso di un oggetto Blob con il MIME type esplicitamente impostato a application/json. Questo approccio è importante perché, per impostazione predefinita, sendBeacon() invia i dati come text/plain quando si passa una stringa, mentre molti backend si aspettano un Content-Type specifico per parsare correttamente il JSON.
Invio di metriche di performance
Le metriche di performance, come i Core Web Vitals (LCP, FID, CLS, INP), rappresentano un altro caso d'uso ideale per sendBeacon(). Queste metriche sono spesso disponibili solo dopo che l'utente ha interagito con la pagina o sta per abbandonarla, rendendo l'invio asincrono affidabile una necessità fondamentale.
// Raccolta e invio di metriche di performance
const performanceCollector = {
metrics: {},
recordMetric(name, value) {
this.metrics[name] = {
value: value,
timestamp: Date.now()
};
},
observePerformance() {
// Osservazione del Largest Contentful Paint
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.recordMetric("LCP", lastEntry.renderTime || lastEntry.loadTime);
});
lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
// Osservazione del Cumulative Layout Shift
let cumulativeLayoutShift = 0;
const clsObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
cumulativeLayoutShift += entry.value;
}
}
this.recordMetric("CLS", cumulativeLayoutShift);
});
clsObserver.observe({ type: "layout-shift", buffered: true });
},
flush() {
if (Object.keys(this.metrics).length === 0) {
return false;
}
const payload = JSON.stringify({
url: window.location.href,
metrics: this.metrics,
connection: navigator.connection ? {
effectiveType: navigator.connection.effectiveType,
rtt: navigator.connection.rtt
} : null
});
const blob = new Blob([payload], { type: "application/json" });
return navigator.sendBeacon("/api/performance", blob);
}
};
performanceCollector.observePerformance();
// Invio delle metriche quando la pagina viene nascosta
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
performanceCollector.flush();
}
});
Tracciamento di interazioni e funnel di conversione
Nel contesto dell'e-commerce e delle applicazioni di marketing, è fondamentale tracciare il comportamento degli utenti lungo il funnel di conversione. Spesso si verifica una situazione in cui un utente compie un'azione critica, come cliccare su un pulsante di acquisto, e immediatamente dopo la pagina viene ricaricata o reindirizzata. In questi casi, sendBeacon() garantisce che l'evento venga registrato correttamente.
// Tracker per il funnel di conversione
class ConversionTracker {
constructor(apiEndpoint) {
this.apiEndpoint = apiEndpoint;
this.userId = this.getUserId();
}
getUserId() {
let id = localStorage.getItem("user_id");
if (!id) {
id = crypto.randomUUID();
localStorage.setItem("user_id", id);
}
return id;
}
trackEvent(eventName, properties = {}) {
const eventData = {
event: eventName,
userId: this.userId,
properties: properties,
timestamp: Date.now(),
page: window.location.pathname
};
const formData = new FormData();
formData.append("payload", JSON.stringify(eventData));
return navigator.sendBeacon(this.apiEndpoint, formData);
}
trackCheckoutStep(step, cartValue) {
return this.trackEvent("checkout_step", {
step: step,
cartValue: cartValue,
currency: "EUR"
});
}
}
const tracker = new ConversionTracker("/api/events");
// Esempio di utilizzo su un pulsante di checkout
document.querySelector("#checkout-button").addEventListener("click", () => {
tracker.trackCheckoutStep("payment_initiated", 99.99);
});
In questo esempio utilizziamo un oggetto FormData per inviare i dati. Questa è una delle modalità supportate da sendBeacon(), che accetta diversi tipi di payload tra cui stringhe, Blob, ArrayBuffer, FormData e URLSearchParams.
Logout e cleanup di sessione
Un caso d'uso meno ovvio ma altrettanto importante riguarda le operazioni di cleanup quando l'utente abbandona una sessione. In applicazioni web complesse, può essere necessario notificare al server la disconnessione dell'utente per liberare risorse, aggiornare lo stato di presenza in tempo reale o salvare lo stato corrente del lavoro non salvato.
// Gestione della disconnessione e dello stato di presenza
class PresenceManager {
constructor(userId, sessionId) {
this.userId = userId;
this.sessionId = sessionId;
this.heartbeatInterval = null;
}
startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
fetch("/api/presence/ping", {
method: "POST",
body: JSON.stringify({ sessionId: this.sessionId })
});
}, 30000);
}
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
}
}
notifyDisconnection() {
const payload = JSON.stringify({
userId: this.userId,
sessionId: this.sessionId,
reason: "page_unload",
timestamp: Date.now()
});
const blob = new Blob([payload], { type: "application/json" });
navigator.sendBeacon("/api/presence/disconnect", blob);
}
initialize() {
this.startHeartbeat();
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
this.stopHeartbeat();
this.notifyDisconnection();
}
});
}
}
const presence = new PresenceManager("user-123", "session-abc");
presence.initialize();
Limitazioni e considerazioni pratiche
Nonostante i suoi vantaggi, sendBeacon() presenta alcune limitazioni che è importante conoscere. Innanzitutto, il metodo supporta esclusivamente richieste POST: non è possibile utilizzarlo per operazioni GET, PUT o DELETE. Inoltre, il browser impone un limite alla dimensione totale dei dati accodati tramite beacon, tipicamente intorno ai 64 KB per richiesta secondo la specifica W3C, anche se l'implementazione può variare leggermente tra i browser.
Un'altra considerazione riguarda l'impossibilità di leggere la risposta del server. A differenza di fetch, sendBeacon() non restituisce una Promise con i dati di risposta, ma solo un booleano che indica l'accodamento della richiesta. Questo significa che il metodo è adatto esclusivamente a scenari "fire and forget" in cui non ci interessa elaborare la risposta del server lato client.
// Pattern di fallback con compatibilità progressiva
function reliableSend(url, data) {
const payload = JSON.stringify(data);
// Tentativo con sendBeacon se disponibile
if (navigator.sendBeacon) {
const blob = new Blob([payload], { type: "application/json" });
const queued = navigator.sendBeacon(url, blob);
if (queued) {
return Promise.resolve({ method: "beacon", success: true });
}
}
// Fallback su fetch con keepalive
return fetch(url, {
method: "POST",
body: payload,
headers: { "Content-Type": "application/json" },
keepalive: true
}).then(() => ({ method: "fetch", success: true }))
.catch(() => ({ method: "fetch", success: false }));
}
Nell'esempio precedente vediamo un pattern di fallback che utilizza fetch con l'opzione keepalive: true quando sendBeacon() non è disponibile o fallisce nell'accodamento. L'opzione keepalive è una valida alternativa moderna che consente alle richieste fetch di sopravvivere alla chiusura del documento, offrendo maggiore flessibilità rispetto a sendBeacon() dato che supporta tutti i metodi HTTP e permette la lettura della risposta.
Quando usare sendBeacon e quando preferire alternative
La scelta tra sendBeacon() e fetch con keepalive dipende dalle esigenze specifiche del progetto. Il primo è preferibile per la sua semplicità d'uso e per la garanzia di compatibilità con browser più datati. Il secondo offre maggiore flessibilità e controllo, risultando più adatto a scenari complessi in cui è necessario gestire header personalizzati, autenticazione tramite token o elaborare la risposta del server.
In generale, sendBeacon() rappresenta la soluzione ottimale per analytics, tracking di eventi, error reporting e qualsiasi altro scenario in cui occorre inviare piccole quantità di dati al server in modo affidabile durante la chiusura della pagina o il cambio di visibilità. La sua semplicità, combinata con il supporto nativo nei browser moderni, lo rende uno strumento indispensabile nel toolkit di ogni sviluppatore web che si occupi di telemetria e monitoraggio applicativo.