Tải mô hình AI xuống bằng Background Fetch API

Ngày xuất bản: 20 tháng 2 năm 2025

Việc tải các mô hình AI có kích thước lớn xuống một cách đáng tin cậy là một việc khó khăn. Nếu mất kết nối Internet hoặc đóng trang web/ứng dụng web của bạn, người dùng sẽ mất các tệp mô hình đã tải xuống một phần và phải bắt đầu lại khi quay lại trang của bạn. Bằng cách sử dụng Background Fetch API làm tính năng nâng cao tăng dần, bạn có thể cải thiện đáng kể trải nghiệm người dùng.

Browser Support

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

Source

Đăng ký trình chạy dịch vụ

Background Fetch API yêu cầu ứng dụng của bạn đăng ký một 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);
  });
}

Kích hoạt quá trình tìm nạp ở chế độ nền

Khi tìm nạp, trình duyệt sẽ hiển thị tiến trình cho người dùng và cung cấp cho họ một phương thức để huỷ quá trình tải xuống. Sau khi quá trình tải xuống hoàn tất, trình duyệt sẽ khởi động service worker và ứng dụng có thể thực hiện hành động với phản hồi.

Background Fetch API thậm chí có thể chuẩn bị quá trình tìm nạp để bắt đầu khi không có mạng. Quá trình tải xuống sẽ bắt đầu ngay khi người dùng kết nối lại. Nếu người dùng chuyển sang chế độ ngoại tuyến, quá trình này sẽ tạm dừng cho đến khi người dùng trực tuyến trở lại.

Trong ví dụ sau, người dùng nhấp vào một nút để tải Gemma 2B xuống. Trước khi tìm nạp, chúng ta sẽ kiểm tra xem mô hình đã được tải xuống và lưu vào bộ nhớ đệm trước đó hay chưa, vì vậy, chúng ta không sử dụng các tài nguyên không cần thiết. Nếu không được lưu vào bộ nhớ đệm, chúng ta sẽ bắt đầu tìm nạp trong nền.

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),
    });
  }
});

Hàm getResourceSize() trả về kích thước tính bằng byte của tệp tải xuống. Bạn có thể triển khai việc này bằng cách đưa ra một yêu cầu 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;
  }
};

Báo cáo tiến trình tải xuống

Sau khi quá trình tìm nạp trong nền bắt đầu, trình duyệt sẽ trả về BackgroundFetchRegistration. Bạn có thể dùng sự kiện progress để thông báo cho người dùng về tiến trình tải xuống.

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}`);
});

Thông báo cho người dùng và ứng dụng khách về việc hoàn tất quá trình tìm nạp

Khi quá trình tìm nạp trong nền thành công, trình chạy dịch vụ của ứng dụng sẽ nhận được sự kiện backgroundfetchsuccess.

Mã sau đây có trong trình chạy dịch vụ. Lệnh gọi updateUI() gần cuối cho phép bạn cập nhật giao diện của trình duyệt để thông báo cho người dùng về việc tìm nạp trong nền thành công. Cuối cùng, hãy thông báo cho ứng dụng về quá trình tải xuống đã hoàn tất, ví dụ: bằng cách sử dụng 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,
          });
        }
      });
    })(),
  );
});

Nhận thông báo từ service worker

Để nhận thông báo thành công đã gửi về quá trình tải xuống hoàn tất trên ứng dụng, hãy lắng nghe các sự kiện message. Sau khi nhận được thông báo từ trình thực thi dịch vụ, bạn có thể làm việc với mô hình AI và lưu trữ mô hình đó bằng 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);
  }
});

Huỷ một hoạt động tìm nạp trong nền

Để cho phép người dùng huỷ một lượt tải xuống đang diễn ra, hãy sử dụng phương thức abort() của BackgroundFetchRegistration.

const registration = await navigator.serviceWorker.ready;
const bgFetch = await registration.backgroundFetch.get(FETCH_ID);
if (!bgFetch) {
  return;
}
await bgFetch.abort();

Lưu mô hình vào bộ nhớ đệm

Lưu mô hình đã tải xuống vào bộ nhớ đệm để người dùng chỉ tải mô hình xuống một lần. Mặc dù Background Fetch API cải thiện trải nghiệm tải xuống, nhưng bạn luôn phải cố gắng sử dụng mô hình nhỏ nhất có thể trong AI phía máy khách.

Những API này kết hợp với nhau giúp bạn tạo ra trải nghiệm AI phía máy khách tốt hơn cho người dùng.

Bản minh hoạ

Bạn có thể xem cách triển khai hoàn chỉnh của phương pháp này trong bản minh hoạmã nguồn của bản minh hoạ.

Bảng điều khiển Ứng dụng của Công cụ cho nhà phát triển của Chrome mở ra để tải Background Fetch xuống.
Với Công cụ dành cho nhà phát triển của Chrome, bạn có thể xem trước các sự kiện liên quan đến quá trình tìm nạp ở chế độ nền đang diễn ra. Bản minh hoạ cho thấy một lượt tải xuống đang diễn ra, đã hoàn thành 17,54 MB, với tổng dung lượng là 1,26 GB. Chỉ báo Tải xuống của trình duyệt cũng cho biết quá trình tải xuống đang diễn ra.

Lời cảm ơn

Hướng dẫn này được xem xét bởi François Beaufort, Andre Bandarra, Sebastian Benz, Maud NalpasAlexandra Klepper.