navigator.sendBeacon(url, data) queues a small POST that survives the page closing. It returns true if queued (64 KiB limit, shared with keepalive fetches) and is Baseline Widely available (since October 2020). Send analytics on visibilitychange, not unload, which mobile browsers often skip and which blocks the back/forward cache (Core Web Vitals).
const startedAt = performance.now();
document.addEventListener('visibilitychange', () => {
if (document.visibilityState !== 'hidden') return;
const body = JSON.stringify({ event: 'leave', ms: Math.round(performance.now() - startedAt) });
navigator.sendBeacon('/beacon', new Blob([body], { type: 'application/json' }));
});When headless Chrome 152 1 leaves the page, the Node server logs:
POST /beacon type=application/json mode=cors body={"event":"leave","ms":1220}A JSON Blob travels in cors mode, so a cross-origin collector must answer a preflight; a plain string does not. Chromium 4,389 's fetchLater() sends automatically when the page goes away.