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

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