TL;DR
-
navigator.sendBeacon
should be used withvisibilitychange
andbeforeunload
events, otherwise you'll lose data -
navigator.sendBeacon
has in-flight data limit and other constraints - Use Beacon API with proper fallback to reliably beacon data to server
What is Beacon
From W3C spec Beacon, Beacon is:
an interface that web developers can use to schedule asynchronous and non-blocking delivery of data that minimizes resource contention with other time-critical operations, while ensuring that such requests are still processed and delivered to destination.
The delivery of data is just an abstract way of saying the browser makes an HTTP request that sends back data to the server. The reason for another API that does HTTP when we already have XMLHttpRequest and Fetch API, is to address a typical challenge web developers have.
There are some HTTP requests from the browser that don't need to read or even wait for the server response, usually event tracking, status update, and analytics data. The characteristics of these type of requests are:
- No need to access HTTP response - send and forget
- Lightweight - should not impact the user experience or taking too much network bandwidth
- Happen in the background without user interaction
- Need to reliably send when closing the page AKA, page unload
Keeping these in mind, the above description of the goals of Beacon API would make more sense.
The explicit goals of the Beacon API are to provide a minimal interface to web developers to specify the data and endpoint, then let the browser coalesces the requests.
Because Beacons do not provide response access in a fire-and-forget way and coalesced by the browser, the browser guarantees to initiate these data delivery requests before page is closed/unloaded, and outlive the page lifecycle.
How to Use
You can use beacon via navigator.sendBeacon()
. A minimal example is given from the W3C spec:
<html>
<script>
// emit non-blocking beacon to record client-side event
function reportEvent(event) {
var data = JSON.stringify({
event: event,
time: performance.now()
});
navigator.sendBeacon('/collector', data);
}
// emit non-blocking beacon with session analytics as the page
// transitions to background state (Page Visibility API)
document.addEventListener('visibilitychange', function() {
if (document.visiblityState === 'hidden') {
var sessionData = buildSessionReport();
navigator.sendBeacon('/collector', sessionData);
}
});
</script>
<body>
<a href='http://www.w3.org/' onclick='reportEvent(this)'>
<button onclick="reportEvent('some event')">Click me</button>
</body>
</html>
MDN has the complete API documentation, go take a look!
Alternatives
People have used alternative ways to do what Beacon API meant to do.
By using XMLHttpRequest
or fetch
, you can POST data periodically in the background, and it's totally fine not to read the response.
Another way is to create an img
element and leverages the fact it makes a GET request to server:
const img = new Image();
img.src = `https://mysite.com?${JSON.stringify(data)}`;
The problem is when the user closes the page, the last request is killed and there's no way to recover. In other words, a significant amount of your analytics data is lost and causes data distortion.
To avoid the closing page problem, a solution is to create a sync
XHR on beforeunload
or unload
events, this is very bad for user experience as it blocks the page unloading - imagine your customers have to wait a noticeable amount of time to close the browser tab.
In fact, beforeunload
and unload
are explicitly said to be legacy API and should be avoided. See Page Lifecycle API > Legacy lifecycle APIs to avoid.
The Confusion
It seems easy, a simpler API that does the work reliably. However, people have had issues in production and not seeing the data being beaconed back as expected. Beacon API is broken post described their experiment setup and the results suggest Beacon API is not working as expected.
Reading through the comments section, the problem becomes clear that Beacon itself never had any issues, it is when to call the API.
MDN added you should use sendBeacon
with visibilitychagne
, not unload
or beforeunload
, after the comment discussions from the above post:
The
navigator.sendBeacon()
method asynchronously sends a small amount of data over HTTP to a web server. Itโs intended to be used in combination with thevisibilitychange
event (but not with theunload
andbeforeunload
events).
Other than blocking the page unloading, the two events unload
and beforeunload
are not reliably fired by the browser as you would expect.
Don't lose user and app state, use Page Visibility summarizes:
-
beforeunload
is of limited value as it only fires on desktop navigation. -
unload
does not fire on mobile and desktop Safari.
Therefore, on all mobile browsers, if you use sendBeacon
on beforeunlaod
:
document.addEventListener('beforeunload', navigatior.sendBeacon(url, data));
The callback function which sends the data is never triggered on mobile when the user swipes away or switches app.
To fix it, you should use visibilitychange
event and beforeunload
together.
A less wrong example looks like:
document.addEventListener('visibilitychange', () => {
if (getState() === 'hidden') {
flushData('hidden');
}
});
window.addEventListener('beforeunload', () => {
flushData('beforeunload');
});
Wait? Didn't we just say we should not use beforeunload
? Firing on beforeunload
is still necessary because Safari bug: visibilitychange:hidden doesn't fire during page navigations which is still active as Safari Version 14.0.2 (16610.3.7.1.9).
In practice, you also need to think about what to do with the fact that some clients not firing beforeunload
and some not firing visibilitychange:hidden
and potentially events you fired between last hidden and page unload, etc.
If you want to play with API and events by yourself and confirm, I've put up a demo at https://github.com/xg-wang/how-to-beacon/. Notice this is not for production, read more below.
More on sendBeacon
Data size limit
The spec (3.1 sendBeacon Method) said:
The user agent MUST restrict the maximum data size to ensure that beacon requests are able to complete quickly and in a timely manner.
The restrict is intentionally vague here because the actual implementation is allowed to be different for different browser vendors.
An important thing to notice is the maximum data size is for in-flight data that the browser has not scheduled to sent. In other words, if a call to navigator.sendBeacon()
returns false
because exceeding the limit quota, trying to call navigator.sendBeacon()
immediately after will not help.
When navigator.sendBeacon()
returns false
, a useful pattern is to fallback to fetch
without the keepalive
flag (more on that later), or xhr
without the sync flag. The drawback is you lose the ability to deliver on page unload, but at least during normal sessions the data is not lost.
If you want to know the actual limit number - it's 64KB (w3c/beacon issue, wpt PR). However, you should not take that as a guarantee!
Delivery is not immediate
Unlike other network API, sendBeacon
can be scheduled and coalesced by the browser. You can certainly contain timestamp data in the beacon payload, but the HTTP request time can be delayed.
It may throw error, be sure to catch
If the url parsing has error, sendBeacon
will throw TypeError
.
Another case is you can't pass reference without binding navigator
:
// โ
let s = navigator.sendBeacon;
s('/track', 'data');
// โ
s = navigator.sendBeacon.bind(navigator);
s('/track', 'data');
- FireFox:
Uncaught TypeError: 'sendBeacon' called on an object that does not implement interface Navigator.
- Safari:
TypeError: Can only call Navigator.sendBeacon on instances of Navigator
- Chrome:
TypeError: Illegal invocation
Server is encouraged to return 204 No Content
From: https://www.w3.org/TR/beacon/#sec-sendBeacon-method
Note
Beacon API does not provide a response callback. The server is encouraged to omit returning a response body for such requests (e.g. respond with 204 No Content).
Fetch keepalive
Beacon API uses Fetch keepalive
under the hood, which is defined in the spec.
fetch('/track', {
method: 'POST',
body: getData(),
keepalive: true,
});
// Same as ๐
navigator.sendBeacon('/track', getData());
This means they share the same data limitation, remember we discussed when falling back to fetch
you don't need to add keepalive
?
But unfortunately keepalive
has limited browser support, while sendBeacon
is available on all modern browsers.
Send Blob data
The second data
param sent with sendBeacon
is BodyInit
, which means you can use Blob
to create the data.
const obj = { hello: 'world' };
const blob = new Blob([JSON.stringify(obj, null, 2)], {
type: 'application/json',
});
navigator.sendBeacon('/track', blob);
When creating a application/json
type request, it is no longer a simple request, and will trigger CORS preflight request. See A practical guide to CORS if you're not familiar with CORS.
Cannot use with compression API
There's a new API you can use to compress data on client side: compression
But it won't work with sendBeacon
or Fetch keepalive
, fetch will throw error when the keepalive
request has stream body.
Service Worker
The service worker can operate async after the original document closes. (Tweeter thread)
Ideally, you can put all the existing data processing logic and beaconing to a service worker, to execute code off the main thread.
End word
Beacon is a simple API, but there are complexities coming from the heart of UI engineering. Use it with caution and always check your data.
Top comments (1)
Had a great conversation with Stef on twitter twitter.com/stefanpenner/status/13...
I'll think more about this topic and write a follow-up.