It's common for web pages to need to send data (or "beacon") back to their server—think analytics data for a user's current session, as an example. For developers, this requires a balancing act: reducing constant, possibly redundant, requests without risking missed data if the tab was closed or the user navigated away before a beacon can be sent.
Traditionally, developers have used pagehide
and visibilitychange
events to catch the page as it unloads, and then use navigator.sendBeacon()
or a fetch()
with keepalive
to beacon data. However, both of these events have difficult corner cases that differ based on the user's browser, and sometimes the events never arrive at all—especially on mobile.
fetchLater()
is a proposal to replace this complexity with a single API call. It does exactly as its name suggests: it asks the browser to ensure a request is made at some point in the future, even if the page is closed or the user navigates away.
fetchLater()
is available in Chrome for testing with real users behind an origin trial starting in version 121 (released in January 2024), running until 3rd September 2024.
The fetchLater()
API
const fetchLaterResult = fetchLater(request, options);
fetchLater()
takes two arguments, generally identical to those of fetch()
:
- The
request
, either a string URL or aRequest
instance. - An optional
options
object, which extends theoptions
fromfetch()
with a timeout calledactivateAfter
.
fetchLater()
returns a FetchLaterResult
, currently containing only a single read-only property activated
, which will be set to true
when "later" has passed and the fetch has been made. Any response to the fetchLater()
request is discarded.
request
The simplest usage is a URL by itself:
fetchLater('/endpoint/');
But, just like fetch()
, a large number of options can be set on a fetchLater()
request, including custom headers, credentials behavior, a POST
body, and an AbortController
signal
to potentially cancel it.
fetchLater('/endpoint/', {
method: 'GET',
cache: 'no-store',
mode: 'same-origin',
headers: {Authorization: 'SUPER_SECRET'},
});
options
The options object extends fetch()
's options with a timeout, activateAfter
, in case you want to fire the request after the timeout or when the page is unloaded, whichever comes first.
This lets you decide the tradeoff between getting data at the absolute last possible moment or when it's more timely.
For example, if you have an app your users usually keep open for the entire work day, you might want to have a timeout of an hour to ensure more granular analytics while still guaranteeing a beacon if the user exited at any time before that hour was up. A new fetchLater()
can then be set up for the next hour of analytics.
const hourInMilliseconds = 60 * 60 * 1000;
fetchLater('/endpoint/', {activateAfter: hourInMilliseconds});
Example use
One issue when measuring Core Web Vitals in the field is that any of the performance metrics could change until the user actually leaves a page. For example, larger layout shifts could happen at any time, or the page could take even longer to respond to an interaction.
However, you don't want to risk losing all performance data due to buggy or incomplete beaconing on page unload. It's a perfect candidate for fetchLater()
.
In this example, the web-vitals.js library is used to monitor the metrics, and fetchLater()
is used to report the results to an analytics endpoint:
import {onCLS, onINP, onLCP} from 'web-vitals';
const queue = new Set();
let fetchLaterController;
let fetchLaterResult;
function updateQueue(metricUpdate) {
// If there was an already complete request for whatever
// reason, clear out the queue of already-sent updates.
if (fetchLaterResult?.activated) {
queue.clear();
}
queue.add(metricUpdate);
// JSON.stringify used here for simplicity and will likely include
// more data than you need. Replace with a preferred serialization.
const body = JSON.stringify([...queue]);
// Abort any existing `fetchLater()` and schedule a new one with
// the update included.
fetchLaterController?.abort();
fetchLaterController = new AbortController();
fetchLaterResult = fetchLater('/analytics', {
method: 'POST',
body,
signal: fetchLaterController.signal,
activateAfter: 60 * 60 * 1000, // Timeout to ensure timeliness.
});
}
onCLS(updateQueue);
onINP(updateQueue);
onLCP(updateQueue);
Every time a metric update comes in, any existing scheduled fetchLater()
is canceled with an AbortController
and a new fetchLater()
is created with the update included.
Try out fetchLater()
As stated, fetchLater()
is available in an origin trial until Chrome 126. See "Get started with origin trials" for background information on origin trials
For local testing, fetchLater
can be enabled with the Experimental Web Platform features flag at chrome://flags/#enable-experimental-web-platform-features
. It can also be enabled by running Chrome from the command line with --enable-experimental-web-platform-features
, or the more targeted --enable-features=FetchLaterAPI
flag.
If you use it on a public page, make sure to feature detect by checking if the global fetchLater
is defined before using it:
if (globalThis.fetchLater) {
// Set up beaconing using fetchLater().
// ...
}
Feedback
Developer feedback is essential to getting new web APIs right, so please file issues and feedback on GitHub.