Download AI models with the Background Fetch API

Published: February 20, 2025

Reliably downloading large AI models is a challenging task. If users lose their internet connection or close your website or web application, they lose partially downloaded model files and have to start over on return to your page. By using the Background Fetch API as a progressive enhancement, you can improve the user experience significantly.

Browser Support

  • Chrome: 74.
  • Edge: 79.
  • Firefox: not supported.
  • Safari: not supported.

Source

Register a service worker

The Background Fetch API requires your app to register a service worker.

if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    const registration = await navigator.serviceWorker.register('sw.js');
    console.log('Service worker registered for scope', registration.scope);
  });
}

Trigger a background fetch

As the browser fetches, it displays progress to the user and gives them a method to cancel the download. Once the download is complete, the browser starts the service worker and the application can take action with the response.

The Background Fetch API can even prepare the fetch to start while offline. As soon as the user reconnects, the download begins. If the user goes offline, the process pauses until the user is online again.

In the following example, the user clicks a button to download Gemma 2B. Before we fetch, we check if the model was previously downloaded and cached, so we don't use unnecessary resources. If it's not cached, we start the background fetch.

const FETCH_ID = 'gemma-2b';
const MODEL_URL =
  'https://storage.googleapis.com/jmstore/kaggleweb/grader/g-2b-it-gpu-int4.bin';

downloadButton.addEventListener('click', async (event) => {
  // If the model is already downloaded, return it from the cache.
  const modelAlreadyDownloaded = await caches.match(MODEL_URL);
  if (modelAlreadyDownloaded) {
    const modelBlob = await modelAlreadyDownloaded.blob();
    // Do something with the model.
    console.log(modelBlob);
    return;
  }

  // The model still needs to be downloaded.
  // Feature detection and fallback to classic `fetch()`.
  if (!('BackgroundFetchManager' in self)) {
    try {
      const response = await fetch(MODEL_URL);
      if (!response.ok || response.status !== 200) {
        throw new Error(`Download failed ${MODEL_URL}`);
      }
      const modelBlob = await response.blob();
      // Do something with the model.
      console.log(modelBlob);
      return;
    } catch (err) {
      console.error(err);
    }
  }

  // The service worker registration.
  const registration = await navigator.serviceWorker.ready;

  // Check if there's already a background fetch running for the `FETCH_ID`.
  let bgFetch = await registration.backgroundFetch.get(FETCH_ID);

  // If not, start a background fetch.
  if (!bgFetch) {
    bgFetch = await registration.backgroundFetch.fetch(FETCH_ID, MODEL_URL, {
      title: 'Gemma 2B model',
      icons: [
        {
          src: 'icon.png',
          size: '128x128',
          type: 'image/png',
        },
      ],
      downloadTotal: await getResourceSize(MODEL_URL),
    });
  }
});

The getResourceSize() function returns byte size of the download. You can implement this by making a HEAD request.

const getResourceSize = async (url) => {
  try {
    const response = await fetch(url, { method: 'HEAD' });
    if (response.ok) {
      return response.headers.get('Content-Length');
    }
    console.error(`HTTP error: ${response.status}`);
    return 0;
  } catch (error) {
    console.error('Error fetching content size:', error);
    return 0;
  }
};

Report download progress

Once the background fetch begins, the browser returns a BackgroundFetchRegistration. You can use this to inform the user of the download progress, with the progress event.

bgFetch.addEventListener('progress', (e) => {
  // There's no download progress yet.
  if (!bgFetch.downloadTotal) {
    return;
  }
  // Something went wrong.
  if (bgFetch.failureReason) {
    console.error(bgFetch.failureReason);
  }
  if (bgFetch.result === 'success') {
    return;
  }
  // Update the user about progress.
  console.log(`${bgFetch.downloaded} / ${bgFetch.downloadTotal}`);
});

Notify users and client of fetch completion

When the background fetch succeeds, your app's service worker receives a backgroundfetchsuccess event.

The following code is included in the service worker. The updateUI() call near the end lets you update the browser's interface to notify the user of the successful background fetch. Finally, inform the client about the finished download, for example, using postMessage().

self.addEventListener('backgroundfetchsuccess', (event) => {
  // Get the background fetch registration.
  const bgFetch = event.registration;

  event.waitUntil(
    (async () => {
      // Open a cache named 'downloads'.
      const cache = await caches.open('downloads');
      // Go over all records in the background fetch registration.
      // (In the running example, there's just one record, but this way
      // the code is future-proof.)
      const records = await bgFetch.matchAll();
      // Wait for the response(s) to be ready, then cache it/them.
      const promises = records.map(async (record) => {
        const response = await record.responseReady;
        await cache.put(record.request, response);
      });
      await Promise.all(promises);

      // Update the browser UI.
      event.updateUI({ title: 'Model downloaded' });

      // Inform the clients that the model was downloaded.
      self.clients.matchAll().then((clientList) => {
        for (const client of clientList) {
          client.postMessage({
            message: 'download-complete',
            id: bgFetch.id,
          });
        }
      });
    })(),
  );
});

Receive messages from the service worker

To receive the sent success message about the completed download on the client, listen for message events. Once you receive the message from the service worker, you can work with the AI model and store it with the Cache API.

navigator.serviceWorker.addEventListener('message', async (event) => {
  const cache = await caches.open('downloads');
  const keys = await cache.keys();
  for (const key of keys) {
    const modelBlob = await cache
      .match(key)
      .then((response) => response.blob());
    // Do something with the model.
    console.log(modelBlob);
  }
});

Cancel a background fetch

To let the user cancel an ongoing download, use the abort() method of the BackgroundFetchRegistration.

const registration = await navigator.serviceWorker.ready;
const bgFetch = await registration.backgroundFetch.get(FETCH_ID);
if (!bgFetch) {
  return;
}
await bgFetch.abort();

Cache the model

Cache downloaded models, so your users only download the model once. While the Background Fetch API improves the download experience, you should always aim to use the smallest possible model in client-side AI.

Together, these APIs help you create a better client-side AI experience for your users.

Demo

You can see a complete implementation of this approach in the demo and its source code.

Chrome DevTools Application panel open to the Background Fetch download.
With Chrome DevTools, you can preview the events related to the ongoing background fetch. The demo shows an ongoing download that's 17.54M megabytes done, with 1.26 gigabytes in total. The browser's Download indicator also shows the ongoing download.

Acknowledgements

This guide was reviewed by François Beaufort, Andre Bandarra, Sebastian Benz, Maud Nalpas, and Alexandra Klepper.