Descarga modelos de IA con la API de Background Fetch

Fecha de publicación: 20 de febrero de 2025

Descargar modelos de IA grandes de forma confiable es una tarea desafiante. Si los usuarios pierden su conexión a Internet o cierran tu sitio web o aplicación web, pierden los archivos de modelos descargados de forma parcial y deben comenzar de nuevo cuando regresan a tu página. Si usas la API de Background Fetch como mejora progresiva, puedes mejorar significativamente la experiencia del usuario.

Browser Support

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

Source

Registra un service worker

La API de Background Fetch requiere que tu app registre un trabajador de servicio.

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

Cómo activar una recuperación en segundo plano

A medida que el navegador recupera datos, muestra el progreso al usuario y le brinda un método para cancelar la descarga. Una vez que se completa la descarga, el navegador inicia el trabajador de servicio y la aplicación puede realizar acciones con la respuesta.

La API de Background Fetch incluso puede preparar la actualización para que comience sin conexión. La descarga comienza en cuanto el usuario se vuelve a conectar. Si el usuario deja de tener conexión, el proceso se pausa hasta que el usuario vuelva a tener conexión.

En el siguiente ejemplo, el usuario hace clic en un botón para descargar Gemma 2B. Antes de recuperarlo, verificamos si el modelo se descargó y almacenó en caché con anterioridad para no usar recursos innecesarios. Si no está almacenado en caché, iniciamos la recuperación en segundo plano.

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

La función getResourceSize() muestra el tamaño en bytes de la descarga. Para implementar esto, realiza una solicitud 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;
  }
};

Informa el progreso de la descarga

Una vez que comienza la recuperación en segundo plano, el navegador muestra un BackgroundFetchRegistration. Puedes usarlo para informar al usuario sobre el progreso de la descarga con el evento 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}`);
});

Notifica a los usuarios y al cliente que se completó la recuperación

Cuando la actualización en segundo plano se realiza correctamente, el trabajador de servicio de tu app recibe un evento backgroundfetchsuccess.

El siguiente código se incluye en el trabajador de servicio. La llamada a updateUI() cerca del final te permite actualizar la interfaz del navegador para notificarle al usuario que la recuperación en segundo plano se realizó correctamente. Por último, informa al cliente sobre la descarga finalizada, por ejemplo, con 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,
          });
        }
      });
    })(),
  );
});

Cómo recibir mensajes del trabajador del servicio

Para recibir el mensaje de éxito enviado sobre la descarga completada en el cliente, escucha los eventos message. Una vez que recibas el mensaje del trabajador del servicio, podrás trabajar con el modelo de IA y almacenarlo con la API de Cache.

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

Cómo cancelar una actualización en segundo plano

Para permitir que el usuario cancele una descarga en curso, usa el método abort() de BackgroundFetchRegistration.

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

Almacena en caché el modelo

Almacena en caché los modelos descargados para que los usuarios solo descarguen el modelo una vez. Si bien la API de Background Fetch mejora la experiencia de descarga, siempre debes intentar usar el modelo más pequeño posible en la IA del cliente.

En conjunto, estas APIs te ayudan a crear una mejor experiencia de IA del cliente para tus usuarios.

Demostración

Puedes ver una implementación completa de este enfoque en la demostración y su código fuente.

Panel de la aplicación de las Herramientas para desarrolladores de Chrome abierto en la descarga de actualización en segundo plano.
Con las Herramientas para desarrolladores de Chrome, puedes obtener una vista previa de los eventos relacionados con la actualización en segundo plano en curso. En la demostración, se muestra una descarga en curso que ya completó 17.54 millones de megabytes, con 1.26 gigabytes en total. El indicador de descargas del navegador también muestra la descarga en curso.

Agradecimientos

Esta guía fue revisada por François Beaufort, Andre Bandarra, Sebastian Benz, Maud Nalpas y Alexandra Klepper.