Introducing Background Fetch
In 2015 we introduced Background Sync which allows the service worker to defer work until the user has connectivity. This means the user could type a message, hit send, and leave the site knowing that the message will be sent either now, or when they have connectivity.
It's a useful feature, but it requires the service worker to be alive for the duration of the fetch. That isn't a problem for short bits of work like sending a message, but if the task takes too long the browser will kill the service worker, otherwise it's a risk to the user's privacy and battery.
So, what if you need to download something that might take a long time, like a movie, podcasts, or levels of a game. That's what Background Fetch is for.
Background Fetch is available by default since Chrome 74.
Here's a quick two minute demo showing the traditional state of things, vs using Background Fetch:
Try the demo yourself and browse the code.
How it works
A background fetch works like this:
- You tell the browser to perform a group of fetches in the background.
- The browser fetches those things, displaying progress to the user.
- Once the fetch has completed or failed, the browser opens your service worker and fires an event to tell you what happened. This is where you decide what to do with the responses, if anything.
If the user closes pages to your site after step 1, that's ok, the download will continue. Because the fetch is highly visible and easily abortable, there isn't the privacy concern of a way-too-long background sync task. Because the service worker isn't constantly running, there isn't the concern that it could abuse the system, such as mining bitcoin in the background.
On some platforms (such as Android) it's possible for the browser to close after step 1, as the browser can hand off the fetching to the operating system.
If the user starts the download while offline, or goes offline during the download, the background fetch will be paused and resumed later.
The API
Feature detect
As with any new feature, you want to detect if the browser supports it. For Background Fetch, it's as simple as:
if ('BackgroundFetchManager' in self) {
// This browser supports Background Fetch!
}
Starting a background fetch
The main API hangs off a service worker registration, so make sure you've registered a service worker first. Then:
navigator.serviceWorker.ready.then(async (swReg) => {
const bgFetch = await swReg.backgroundFetch.fetch('my-fetch', ['/ep-5.mp3', 'ep-5-artwork.jpg'], {
title: 'Episode 5: Interesting things.',
icons: [{
sizes: '300x300',
src: '/ep-5-icon.png',
type: 'image/png',
}],
downloadTotal: 60 * 1024 * 1024,
});
});
Many examples in this article use async functions. If you aren't familiar with them, check out the guide.
backgroundFetch.fetch
takes three arguments:
Parameters | |
---|---|
id | string uniquely identifies this background fetch.
|
requests | Array<Request|string> The things to fetch. Strings will be treated as URLs, and turned into Request s via new Request(theString) .You can fetch things from other origins as long as the resources allow it via CORS. Note: Chrome doesn't currently support requests that would require a CORS preflight. |
options | An object which may include the following: |
options.title | string A title for the browser to display along with progress. |
options.icons | Array<IconDefinition> An array of objects with a `src`, `size`, and `type`. |
options.downloadTotal | number The total size of the response bodies (after being un-gzipped). Although this is optional, it's strongly recommended that you provide it. It's used to tell the user how big the download is, and to provide progress information. If you don't provide this, the browser will tell the user the size is unknown, and as a result the user may be more likely to abort the download. If the background fetch downloads exceeds the number given here, it will be aborted. It's totally fine if the download is smaller than the |
backgroundFetch.fetch
returns a promise that resolves with a BackgroundFetchRegistration
. I'll cover the details of that later. The promise rejects if the user has opted out of downloads, or one of the provided parameters is invalid.
Providing many requests for a single background fetch lets you combine things that are logically a single thing to the user. For example, a movie may be split into 1000s of resources (typical with MPEG-DASH), and come with additional resources like images. A level of a game could be spread across many JavaScript, image, and audio resources. But to the user, it's just "the movie", or "the level".
Caution: Chrome's implementation currently only accepts requests without a body. In future, bodies will be allowed, meaning you can use background fetch for large uploads, such as photos and video.
Getting an existing background fetch
You can get an existing background fetch like this:
navigator.serviceWorker.ready.then(async (swReg) => {
const bgFetch = await swReg.backgroundFetch.get('my-fetch');
});
…by passing the id of the background fetch you want. get
returns undefined
if there's no active background fetch with that ID.
A background fetch is considered "active" from the moment it's registered, until it either succeeds, fails, or is aborted.
You can get a list of all the active background fetches using getIds
:
navigator.serviceWorker.ready.then(async (swReg) => {
const ids = await swReg.backgroundFetch.getIds();
});
Background fetch registrations
A BackgroundFetchRegistration
(bgFetch
in the above examples) has the following:
Properties | |
---|---|
id | string The background fetch's ID. |
uploadTotal | number The number of bytes to be sent to the server. |
uploaded | number The number of bytes successfully sent. |
downloadTotal | number The value provided when the background fetch was registered, or zero. |
downloaded | number The number of bytes successfully received. This value may decrease. For example, if the connection drops and the download cannot be resumed, in which case the browser restarts the fetch for that resource from scratch. |
result | One of the following:
|
failureReason | One of the following:
|
recordsAvailable | boolean Can the underlying requests/responses can be accessed? Once this is false |
Methods | |
abort() | Returns Promise<boolean> Abort the background fetch. The returned promise resolves with true if the fetch was successfully aborted. |
matchAll(request, opts) | Returns Promise<Array<BackgroundFetchRecord>> Get the requests and responses. The arguments here are the same as the cache API. Calling without arguments returns a promise for all records. See below for more details. |
match(request, opts) | Returns Promise<BackgroundFetchRecord> As above, but resolves with the first match. |
Events | |
progress | Fired when any of uploaded , downloaded , result , or failureReason change. |
Tracking progress
This can be done via the progress
event. Remember that downloadTotal
is whatever value you provided, or 0
if you didn't provide a value.
bgFetch.addEventListener('progress', () => {
// If we didn't provide a total, we can't provide a %.
if (!bgFetch.downloadTotal) return;
const percent = Math.round(bgFetch.downloaded / bgFetch.downloadTotal * 100);
console.log(`Download progress: ${percent}%`);
});
Getting the requests and responses
In Chrome's current implementation you can only get the requests and responses during backgroundfetchsuccess
, backgroundfetchfailure
, and backgroundfetchabort
service worker events (see below). In future you'll be able to get in-progress fetches.
bgFetch.match('/ep-5.mp3').then(async (record) => {
if (!record) {
console.log('No record found');
return;
}
console.log(`Here's the request`, record.request);
const response = await record.responseReady;
console.log(`And here's the response`, response);
});
record
is a BackgroundFetchRecord
, and it looks like this:
Properties | |
---|---|
request | Request The request that was provided. |
responseReady | Promise<Response> The fetched response. The response is behind a promise because it may not have been received yet. The promise will reject if the fetch fails. |
Service worker events
Events | |
---|---|
backgroundfetchsuccess | Everything was fetched successfully. |
backgroundfetchfailure | One or more of the fetches failed. |
backgroundfetchabort | One or more fetches failed. This is only really useful if you want to perform clean-up of related data. |
backgroundfetchclick | The user clicked on the download progress UI. |
The event objects have the following:
Properties | |
---|---|
registration | BackgroundFetchRegistration |
Methods | |
updateUI({ title, icons }) | Lets you change the title/icons you initially set. This is optional, but it lets you provide more context if necessary. You can only do this *once* during backgroundfetchsuccess and backgroundfetchfailure events. |
Reacting to success/failure
We've already seen the progress
event, but that's only useful while the user has a page open to your site. The main benefit of background fetch is things continue to work after the user leaves the page, or even closes the browser.
If the background fetch successfully completes, your service worker will receive the backgroundfetchsuccess
event, and event.registration
will be the background fetch registration.
After this event, the fetched requests and responses are no longer accessible, so if you want to keep them, move them somewhere like the cache API.
As with most service worker events, use event.waitUntil
so the service worker knows when the event is complete.
Note: You can't hold the service worker open indefinitely here, so avoid doing things that would keep the service worker open a long time here, such as additional fetching.
For example, in your service worker:
addEventListener('backgroundfetchsuccess', (event) => {
const bgFetch = event.registration;
event.waitUntil(async function() {
// Create/open a cache.
const cache = await caches.open('downloads');
// Get all the records.
const records = await bgFetch.matchAll();
// Copy each request/response across.
const promises = records.map(async (record) => {
const response = await record.responseReady;
await cache.put(record.request, response);
});
// Wait for the copying to complete.
await Promise.all(promises);
// Update the progress notification.
event.updateUI({ title: 'Episode 5 ready to listen!' });
}());
});
Failure may have come down to a single 404, which may not have been important to you, so it might still be worth copying some responses into a cache as above.
Reacting to click
The UI displaying the download progress and result is clickable. The backgroundfetchclick
event in the service worker lets you react to this. As above event.registration
will be the background fetch registration.
The common thing to do with this event is open a window:
addEventListener('backgroundfetchclick', (event) => {
const bgFetch = event.registration;
if (bgFetch.result === 'success') {
clients.openWindow('/latest-podcasts');
} else {
clients.openWindow('/download-progress');
}
});
Additional resources
Correction: A previous version of this article incorrectly referred to Background Fetch as being a "web standard". The API is not currently on the standards track, the specification can be found in WICG as a Draft Community Group Report.