使用 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 开发者工具,您可以预览与正在进行的后台提取相关的事件。此演示显示了一个正在进行的下载,已完成 17.54 兆字节,总共 1.26 吉字节。浏览器的“下载”指示器也会显示正在进行的下载。

致谢

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