La maggior parte dei dati di analisi, telemetria e Core Web Vitals ha una priorità inferiore rispetto alla risposta all'utente, quindi puoi rimandare l'invio fino a quando l'utente non lascia la pagina. L'eccezione a questa regola è se devi fornire aggiornamenti in tempo reale, nel qual caso la tempestività è importante.
Il modo ottimale per fornire aggiornamenti di analisi in tempo reale, riducendo al minimo i beacon, è utilizzare fetchLater() con l'opzione di configurazione activateAfter. In questo modo, puoi raggruppare in modo efficace tutti gli eventi di analisi che si verificano in un determinato intervallo di tempo in un unico beacon, che viene inviato in modo affidabile anche se l'utente lascia la pagina prima della scadenza del timeout.
Come implementare
Pianifica la richiesta: non appena sono disponibili dati di analisi pertinenti, chiama
fetchLater()con il payload dei dati e passa un valoreactivateAfter. In questo modo, i dati vengono messi in coda per essere inviati dopo questo periodo di tempo o se l'utente abbandona la pagina prima.Raggruppa più eventi: se si verifica un nuovo evento di analisi prima della scadenza del timeout
activateAfter, interrompi la richiesta pianificata in precedenza e chiama di nuovofetchLater()(con lo stesso valoreactivateAfter) con la coda di eventi completa in un unico payload.Reimposta la coda degli eventi alla scadenza del timeout: se si verifica un nuovo evento di analisi dopo che l'invio del beacon pianificato è andato a buon fine (ovvero il valore
activateddel risultatofetchLater()ètrue), reimposta la coda degli eventi.Lascia che il browser gestisca il resto: se l'utente esce dalla pagina o chiude la scheda prima della scadenza del timeout di
activateAfter, il browser invierà comunque in modo affidabile il payload della chiamatafetchLater()più recente.
Esempio di codice
Questo codice monitora tutti gli eventi load e click su una pagina e raggruppa tutti gli eventi che si verificano entro un timeout di 10 secondi.
// Replace with your analytics endpoint.
const ANALYTICS_ENDPOINT = '/path/to/analytics/endpoint';
// Replace with a time window of your choice. All analytics events that
// occur within this time window will be batched together.
const BATCH_WINDOW = 10 * 1000;
// The maximum number of events to batch. Pick a number that is unlikely
// to overflow the fetchLater() quota for the page.
const MAX_QUEUE_SIZE = 100;
const eventQueue = [];
let fetchLaterResult;
let fetchLaterController;
function trackEvent(eventData) {
// If the previously queued beacon has already been sent, or if the
// max queue size has been met, reset the queue.
if (fetchLaterResult?.activated || eventQueue.length > MAX_QUEUE_SIZE) {
fetchLaterController = null;
fetchLaterResult = null;
eventQueue.length = 0;
}
eventQueue.push(eventData);
// Abort any pending beacons before creating a new one.
if (fetchLaterController) {
fetchLaterController.abort();
}
fetchLaterController = new AbortController();
// Schedule a fetch for the events to be sent when the batch window expires.
// IMPORTANT: wrap the call in a try/catch to handle quota errors.
try {
fetchLaterResult = fetchLater(ANALYTICS_ENDPOINT, {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify(eventQueue),
signal: fetchLaterController.signal,
activateAfter: BATCH_WINDOW,
});
} catch (error) {
// Handle errors as needed.
}
}
// Track page loads.
window.addEventListener('load', () => {
trackEvent({type: 'page_load'});
});
// Track click events.
window.addEventListener('click', (event) => {
trackEvent({type: 'click', target: serializeElement(event.target)});
});
Best practice
- SÌ: utilizza
fetchLater()con l'opzioneactivateAfterimpostata per raggruppare più eventi di analisi che si verificano a breve distanza l'uno dall'altro. - SÌ: utilizza un
AbortControllerper annullare il recupero in attesa nei casi in cui si verifica un nuovo evento di analisi. - SÌ: verifica di non raggruppare troppi eventi insieme e di non superare la quota di
fetchLater()(attualmente circa 64 kB per origine). - SÌ: inserisci le chiamate a
fetchLater()in un bloccotry/catchper gestire gli errori di quota. - SÌ: esegui il rilevamento delle funzionalità per verificare la presenza di
fetchLater()suglobalThise implementa una strategia di riserva per i browser che non supportano l'API. - NON utilizzare un oggetto
ReadableStreamper il corpo della richiesta, in quanto ciò genererà un errore.
Supporto del browser e strategie di riserva
Pertanto, in genere è necessaria una strategia di riserva.
Tuttavia, dati i vantaggi in termini di affidabilità e prestazioni di questa API, è consigliabile utilizzare fetchLater() se il browser la supporta.
fetchLater() polyfill
Utilizza il seguente polyfill fetchLater() minimo, che implementa l'API nel modo più fedele possibile nei browser non supportati.
L'unica differenza di comportamento degna di nota con questo polyfill è che, anziché inviare il payload quando l'utente abbandona la pagina, lo invia ogni volta che la visibilityState della pagina diventa "hidden", poiché questo è l'indicatore di fine sessione più affidabile attualmente disponibile.
globalThis.fetchLater ??= function fetchLater(url, init = {}) {
let timeoutHandle;
let activated = false;
function sendNow() {
if (!(init.signal && init.signal.aborted)) {
// Use fetch keepalive if the browser supports it or if custom fetch
// parameters are specified (e.g. custom headers or methods).
// Otherwise fall back to `navigator.sendBeacon()`.
if (
'keepalive' in Request.prototype ||
init.method !== 'POST' ||
init.headers
) {
fetch(url, Object.assign({}, init, {keepalive: true}));
activated = true;
} else {
activated = navigator.sendBeacon(url, init.body);
}
}
destroy();
}
function destroy() {
document.removeEventListener('visibilitychange', sendNow);
clearTimeout(timeoutHandle);
}
if (document.visibilityState === 'hidden') {
// If the beacon was created while the page is already hidden, send data
// ASAP but wait until the next microtask to allow all sync code to run.
queueMicrotask(sendNow);
} else {
document.addEventListener('visibilitychange', sendNow);
if (typeof init.activateAfter === 'number' && init.activateAfter >= 0) {
timeoutHandle = setTimeout(sendNow, init.activateAfter);
}
}
if (init.signal) {
init.signal.addEventListener('abort', destroy);
}
return {
get activated() {
return activated;
},
};
};