Introducing Background Fetch

Jake Archibald
Jake Archibald

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:

  1. You tell the browser to perform a group of fetches in the background.
  2. The browser fetches those things, displaying progress to the user.
  3. 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,
  });
});

backgroundFetch.fetch takes three arguments:

Parameters
id string
uniquely identifies this background fetch.

backgroundFetch.fetch will reject if the ID matches an existing background fetch.

requests Array<Request|string>
The things to fetch. Strings will be treated as URLs, and turned into Requests 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 downloadTotal, so if you aren't sure what the download total will be, it's best to err on the side of caution.

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".

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:

  • "" - The background fetch is active, so there's no result yet.
  • "success" - The background fetch was successful.
  • "failure" - The background fetch failed. This value only appears when the background fetch totally fails, as in the browser cannot retry/resume.
failureReason

One of the following:

  • "" - The background fetch hasn't failed.
  • "aborted" – The background fetch was aborted by the user, or abort() was called.
  • "bad-status" - One of the responses had a not-ok status, e.g. 404.
  • "fetch-error" - One of the fetches failed for some other reason, e.g. CORS, MIX, an invalid partial response, or a general network failure for a fetch that cannot be retried.
  • "quota-exceeded" - Storage quota was reached during the background fetch.
  • "download-total-exceeded" - The provided `downloadTotal` was exceeded.
recordsAvailable boolean
Can the underlying requests/responses can be accessed?

Once this is false match and matchAll cannot be used.

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

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.

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.