发布时间:2025 年 2 月 20 日
可靠地下载大型 AI 模型是一项具有挑战性的任务。如果用户的互联网连接中断或关闭您的网站或 Web 应用,他们会丢失部分下载的模型文件,并且在返回您的网页后必须重新开始。通过将 Background Fetch API 用作渐进式增强功能,您可以显著提升用户体验。
注册 Service Worker
Background Fetch API 要求您的应用注册服务工件。
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);
  });
}
触发后台提取
浏览器在提取内容时,会向用户显示进度,并为用户提供取消下载的方法。下载完成后,浏览器会启动服务工件,应用可以根据响应执行操作。
Background Fetch API 甚至可以在离线状态下准备提取操作。用户重新连接后,下载就会开始。如果用户离线,该过程会暂停,直到用户重新上线。
在以下示例中,用户点击一个按钮以下载 Gemma 2B。在提取之前,我们会检查模型是否之前已下载并缓存,以免使用不必要的资源。如果未缓存,我们会启动后台提取。
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),
    });
  }
});
getResourceSize() 函数会返回下载内容的字节大小。您可以通过发出 HEAD 请求来实现此操作。
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;
  }
};
报告下载进度
后台提取开始后,浏览器会返回 BackgroundFetchRegistration。您可以使用 progress 事件告知用户下载进度。
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}`);
});
通知用户和客户端提取完成
后台提取成功后,应用的服务工件会收到 backgroundfetchsuccess 事件。
服务工件中包含以下代码。通过底部附近的 updateUI() 调用,您可以更新浏览器的界面,以通知用户后台提取成功。最后,告知客户端下载已完成,例如使用 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,
          });
        }
      });
    })(),
  );
});
接收来自服务工件的邮件
如需在客户端上接收有关已完成下载的成功发送消息,请监听 message 事件。收到来自服务工作器的消息后,您就可以使用 AI 模型并使用 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);
  }
});
取消后台提取
如需让用户取消正在进行的下载,请使用 BackgroundFetchRegistration 的 abort() 方法。
const registration = await navigator.serviceWorker.ready;
const bgFetch = await registration.backgroundFetch.get(FETCH_ID);
if (!bgFetch) {
  return;
}
await bgFetch.abort();
缓存模型
缓存已下载的模型,以便用户只下载一次模型。虽然 Background Fetch API 可以改善下载体验,但您应始终力求在客户端 AI 中使用尽可能小的模型。
这些 API 相辅相成,可帮助您为用户打造更好的客户端 AI 体验。
演示
 
致谢
本指南由 François Beaufort、Andre Bandarra、Sebastian Benz、Maud Nalpas 和 Alexandra Klepper 审核。
