Users are so annoying! There they go, leaving the page before we can save the data to the API. Back in the old days, we would attach a window.unload handler and try to send an AJAX request, but this was slow and unreliable.

Enter the Beacon API to give us a better way.

The Beacon provides a reliable way to send a small amount of data after the page has been unloaded. It’s not bound to the lifetime of the page and it doesn’t slow down the user.

Here is the source for the beacon demo on GitHub.

Using sendBeacon

The Beacon is exposed on the navigator object as sendBeacon. Browser compatibility is really good, except of course for Internet Explorer. Still, we should do some feature detection to make sure it works so our JavaScript doesn’t throw an error.

If your JavaScript throws errors, as most JavaScript does, check out our other product, TrackJS, the fastest and easiest way to find and fix bugs on your production websites.

We call sendBeacon with the URL and the text string to be sent. The browser handles the rest. We still want to attach our call to sendBeacon to the window unload event, so that we queue our data to be sent as the page is closing.

Here is a simple example:

(function client() {
    if (!navigator.sendBeacon) { return; }

    function onUnload() {
        var data = { "foo": "bar" };
        var text = JSON.stringify(data);

        navigator.sendBeacon("http://api.yourservice.com/beacon", text);
    }

    window.addEventListener("unload", onUnload);
})();
Simple example of using sendBeacon

Showing Beacon Requests in DevTools

Running this code, you may not see the beacon requests in your devTools. This is because the beacon requests often happen after the page has unloaded, and the devTools history has been cleared. Turn on “Persist Logs” in your network panel, and you should see the beacon responses.

In Chrome, there is a longstanding bug where beacon requests are always shown in “(Pending)” status. This is expected, and it doesn’t mean your beacon is broken :).

Changing the Content-Type

If you examine the network requests for the beacon, you’ll see that the requests are being sent with a Content-Type header of “text/plain”, which isn’t strictly correct because we are sending a JSON string. Many server technologies will automatically detect and parse the content if sent with the correct headers, so it would be nice if we could correct the Content-Type to “application/json”.

In the MDN page on beacon, it shows an example using a Blob to set the Content-Type:

(function client() {
    if (!navigator.sendBeacon) { return; }

    function onUnload() {
        var data = { "foo": "bar" };
        var blob = new Blob([JSON.stringify(data)], { type: "application/json" });

        navigator.sendBeacon("http://api.yourservice.com/beacon", blob);
    }

    window.addEventListener("unload", onUnload);
})();
Using sendBeacon with a Content-Type

Unfortunately, this DOES NOT WORK.

Setting the Content-Type requires this request to include the Cross-Origin Resource Sharing (CORS) headers, which are a Pain-In-The-Ass (PITA).

Not only do the CORS headers need to be properly configured, but we would need to ensure that a request has been sent to our API before we attempt to send the beacon in order to handle the CORS preflight request.

Even if you jump through all these hoops, it’s not going to work in Chrome anyway. As of Chrome 39, this behavior has been disabled due to a security concern.

So stick with text/plain and handle the JSON serialization yourself.

Dealing with Safari

Safari needs to be different. Safari does not always trigger the unload event due to performance concerns, especially when navigating to new domains and on mobile devices.

Instead, Safari will trigger the pagehide event each time the page loses focus, including on navigation. Compatibility of the pagehide event is not well documented, but it worked for me on Chrome 81, Firefox 76, and Safari 13.

We can modify our code to listen to either event, which should cover all the browser cases.

(function client() {
    if (!navigator.sendBeacon) { return; }

    function onUnload() {
        if (onUnload._hasUnloaded) { return; }
        onUnload._hasUnloaded = true;

        var data = { "foo": "bar" };
        var text = JSON.stringify(data);

        navigator.sendBeacon("http://api.yourservice.com/beacon", text);
    }

    window.addEventListener("unload", onUnload);
    window.addEventListener("pagehide", onUnload);
})();
Using sendBeacon with Safari Support

Wrapping Up

The Beacon API lets us send data to our API after the user has closed the page. At Request Metrics, we use this to send out final performance data about the page. Subscribe to our YouTube channel where we’ve documented our journey building Request Metrics.