A lighthouse against a stary night sky.

Safari’s Beacon API problems

The Beacon API is available in all current mainstream web browsers, and was introduced in Safari in version 11.11 (iOS 11.3 and macOS 10.13). However, Safari have had some trouble delivering on the promises of this web API; especially on iOS.

The Beacon API is a web platform standard that was designed to allow webpages to send some data in an asynchronous POST request without causing a performance penalty for the visitor. It’s being used for all sorts of things, but its main benefit over other network request methods is that it allows for queuing a submission as the visitor leaves the page or the page is being unloaded.

Many examples of the Beacon API execute it in a handler listening to either the beforeunload or unload events. These two events are fired as pages are being unloaded, and notably attaching event listeners to these events causes webpages to be unloaded when navigating away from them. This in turns means the pages can’t be cached and quickly restored when visitors move back and forth in their browser history or restore a previous browsing session.

These events are also unsupported in iOS Safari. These are standard events and part of the web platform, but WebKit has chosen not to emit them. Because these events don’t work on iOS Safari, people misunderstand what’s going on and assume that the Beacon API is to blame when they fail to queue a request in event handlers attached to these events. Safari on macOS can use the beforeunload event but I’d discourage it because of its performance impact.

So what is a web developer to do instead? Use the pagehide event instead. The event is issued when people navigate away from your page even when it’s not being unloaded and it doesn’t have the performance penalty of the other two events. Browser support is as good if not better than both the beforeunload and unload events.

While this event is supported in iOS Safari, you haven’t been able to make any network requests in an attached event handler in Safari version 11.1 up to and including 12.1. The Beacon API was explicitly designed to allow the asynchronous queuing of network requests when navigating away from pages. This was fixed in Safari 12.2 (iOS 12.2 and macOS 10.14.4).

The four event candidates the web platform gives us (in order) are beforeunload and unload (when unloading), pagehide, and visibilitychange. The first two are unsupported on iOS, the third didn’t work until Safari 12.2, so what about the forth?

The Page Visibility API gives us a visibilitychange event after the pagehide event. However, neither macOS Safari or iOS Safari fires this event when navigating away from a page. It’s only fired as intended when pages go in and out of view (minimizing, off-screen, etc.). Chromium has the same behavior, and only Firefox implements the full specification here.

Your only option is to use the pagehide event from Safari 12.2 and later, or to send your beacon earlier in the page lifecycle. This makes it difficult to track reading-depth and time-on-page, unfortunately. This is probably the event you want to use to capture page transitions in all browsers and on all platforms regardless.

Below is the obligatory implementation example (be sure to read on further, though!)

function handle_pagehide() {
  navigator.sendBeacon(
    'https://example.net/beacon',
    {'demo': 'data'}
  )
}

window.addEventListener(
  'pagehide',
  handle_pagehide
);

There are also a couple of other bugs that might get you when working with the Beacon API in Safari on iOS devices.

Safari on iOS also can’t submit a beacon to an previously unvisited secure origin. A bug prevents it from validating HTTPS certificate chains for unknown origins. This issue was fixed in Safari 13 (iOS 13 and macOS 10.15). You can work around this issue by loading a resource from the beacon submission origin before you attempt to submit a beacon request to it.

The fun doesn’t end there. Safari on iOS will also fail to queue beacons for pages that quickly redirect to another location. This may be an anti-tracking feature (Anti-Bounce Tracking?), though it may just be an optimization that’s throwing web platform compatibility to the winds. macOS Safari doesn’t exhibit the same behavior.

In summary: Safari on iOS 13 should work much better with the Beacon API assuming you use it correctly. There isn’t much to be done with older versions, unfortunately. Alternatives like an asynchronous XmlHttpRequest or fetch won’t work in these versions either. The future for the Beacon API on iOS 13+ is looking bright like a lighthouse beacon, though!