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.
The Beacon is exposed on the
navigator object as
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
unload event, so that we queue our data to be sent as the page is closing.
Here is a simple example:
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
Unfortunately, this DOES NOT WORK.
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.
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.