使用 Background Fetch API 下載 AI 模型

發布日期:2025 年 2 月 20 日

穩定下載大型 AI 模型是一項艱鉅的任務。如果使用者無法連上網際網路或關閉網站或網頁應用程式,就會失去部分下載的模型檔案,並必須在返回網頁時重新開始。使用 Background Fetch API 做為漸進式增強功能,可大幅改善使用者體驗。

Browser Support

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

Source

註冊 Service Worker

背景擷取 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();

快取模型

快取已下載的模型,讓使用者只下載一次模型。雖然背景擷取 API 可改善下載體驗,但您應盡量在用戶端 AI 中使用最小型模型。

這些 API 可搭配使用,協助您為使用者打造更優質的用戶端 AI 體驗。

示範

您可以在示範原始碼中,查看這項做法的完整實作方式。

Chrome 開發人員工具「應用程式」面板開啟至「背景擷取」下載。
您可以使用 Chrome 開發人員工具,預覽與目前背景擷取作業相關的事件。這個示範顯示正在進行的下載作業,已完成 17.54 MB,總共 1.26 GB。瀏覽器的下載指標也會顯示正在進行的下載作業。

特別銘謝

本指南由 François BeaufortAndre BandarraSebastian BenzMaud NalpasAlexandra Klepper 審查。