使用 Background Fetch API 下载 AI 模型

发布时间:2025 年 2 月 20 日

可靠地下载大型 AI 模型是一项具有挑战性的任务。如果用户的互联网连接中断或关闭您的网站或 Web 应用,他们会丢失部分下载的模型文件,并且在返回您的网页后必须重新开始。通过将 Background Fetch API 用作渐进式增强功能,您可以显著提升用户体验。

Browser Support

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

Source

注册 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);
  }
});

取消后台提取

如需让用户取消正在进行的下载,请使用 BackgroundFetchRegistrationabort() 方法。

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 体验。

演示

您可以在演示及其源代码中查看此方法的完整实现。

Chrome 开发者工具“应用”面板,其中显示了“后台提取”下载内容。
借助 Chrome 开发者工具,您可以预览与正在进行的后台提取相关的事件。演示中显示了正在进行的下载,已完成 1754 兆字节,总共 1.26 千兆字节。浏览器的“下载”指示器也会显示正在进行的下载。

致谢

本指南由 François BeaufortAndre BandarraSebastian BenzMaud NalpasAlexandra Klepper 审核。