Improving page dismissal in synchronous XMLHttpRequest()

Reducing delayed navigations

Joe Medley
Joe Medley

It's common for a page or app to have unsubmitted analytics or other data at the time a user closes it. To prevent data loss, some sites use a synchronous call to XMLHttpRequest() to keep the page or app open until its data is passed to the server. Not only are there better ways to save data, but this technique creates a bad user experience by delaying closing of the page for up to several seconds.

This practice needs to change, and browsers are responding. The XMLHttpRequest() specification is already slated for deprecation and removal. Chrome 80 takes the first step by disallowing synchronous calls inside several event handlers, specifically beforeunload, unload, pagehide, and visibilitychange when they are fired in the dismissal. WebKit also recently landed a commit implementing the same behavior change.

In this article I'll briefly describe options for those who need time to update their sites and outline the alternatives to XMLHttpRequest().

Temporary opt-outs

Chrome does not simply want to pull the plug on XMLHttpRequest(), which is why a few temporary opt-out options are available. For sites on the internet, an origin trial is available. With this, you add an origin-specific token to your page headers that enables synchronous XMLHttpRequest() calls. This option ends shortly before Chrome 89 ships, sometime in March 2021. Enterprise Chrome customers can also use the AllowSyncXHRInPageDismissal policy flag, which ends at the same time.

Alternatives

Regardless of how you send data back to the server, it's best to avoid waiting until page unload to send all the data at once. Aside from creating a bad user experience, unload is unreliable on modern browsers and risks data loss if something goes wrong. Specifically, unload events often don't fire on mobile browsers because there are many ways to close a tab or browser on mobile operating systems without the unload event firing. With XMLHttpRequest(), using small payloads was a choice. Now it's a requirement. Both of its alternatives have an upload limit of 64 KB per context, as required by the specification.

Fetch keepalive

The Fetch API provides a robust means of dealing with server interactions and a consistent interface for use across different platform APIs. Among its options is keepalive, which ensures that a request continues whether or not the page that made it stays open:

window.addEventListener('unload', {
  fetch('/siteAnalytics', {
    method: 'POST',
    body: getStatistics(),
    keepalive: true
  });
}

The fetch() method has the advantage of greater control over what's sent to the server. What I don't show in the example is that fetch() also returns a promise that resolves with a Response object. Since I'm trying to get out of the way of the page's unloading, I chose not to do anything with it.

SendBeacon()

SendBeacon() actually uses the Fetch API under the hood, which is why it has the same 64 KB payload limitation and why it also ensures that a request continues after a page unload. Its primary advantage is its simplicity. It lets you submit your data with a single line of code:

window.addEventListener('unload', {
  navigator.sendBeacon('/siteAnalytics', getStatistics());
}

Conclusion

With the increased availability of fetch() across browsers, XMLHttpRequest() will hopefully be removed from the web platform at some point. Browser vendors agree it should be removed, but it will take time. Deprecating one of its worst use cases is a first step that improves the user experience for everyone.

Photo by Matthew Hamilton on Unsplash