When building modern web applications with Next.js App Router or React, frontend performance is paramount. We spend hours optimizing images, minimizing CSS, and code-splitting bundles to achieve 100/100 Google Lighthouse scores.
Yet, many developers unknowingly install APM and error tracking SDKs that add 100KB+ of minified JavaScript to their client bundles [1.2.2].
Even worse: many legacy trackers use heavy fetch() wrappers with complex retry queues that can block the browser's main thread during page unloads or hydration.
Here is why we built the SnapTrace telemetry client using navigator.sendBeacon and on-device regex scrubbing to keep the entire client under 5KB.
The Problem with Traditional Fetch for Error Logging
When an uncaught exception crashes a page or when a user closes a tab after a bug, standard fetch() calls face two issues:
-
Lost Telemetry on Page Unload: If a user navigates away or closes the tab immediately after an error, standard asynchronous
fetch()requests are often aborted by the browser before they reach the server. - Main Thread Overhead: Heavy SDKs bundle full DOM serializers, session recorders, and distributed tracing polyfills that eat up CPU cycles during initial hydration.
The Solution: Asynchronous navigator.sendBeacon
The browser's native navigator.sendBeacon() API was designed specifically for analytics and telemetry:
javascript
// Native, zero-dependency async telemetry delivery
function dispatchTelemetry(payload, endpoint, apiKey) {
const targetUrl = `${endpoint}?apiKey=${encodeURIComponent(apiKey)}`;
const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
// Preferred delivery: sendBeacon queues data in browser background
if (navigator.sendBeacon) {
const queued = navigator.sendBeacon(targetUrl, blob);
if (queued) return;
}
// Fallback: fetch with keepalive flag
fetch(targetUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
body: JSON.stringify(payload),
keepalive: true
}).catch(() => {});
}
Top comments (0)