</> HTML5Advent
ENFRESDEITPT

// apis · Web Platform Advent #4

Service Workers: caché, sin conexión y actualizaciones

Guía práctica de service workers — registro, ciclo de vida install/activate, Cache API, servir una página sin conexión y desplegar actualizaciones sin romper nada.

La pantalla de un smartphone en modo avión mostrando la hora, sin conexión de red

Un service worker es un script que el navegador ejecuta en segundo plano, independiente de tu página. Se sitúa entre tu aplicación y la red como un proxy programable: puede interceptar peticiones, responderlas desde una caché y mantener tu sitio funcionando incluso sin conexión. Es la base de las Progressive Web Apps.

Dos reglas fundamentales antes de nada. Un service worker solo se ejecuta sobre HTTPS (o en localhost durante el desarrollo), y no tiene acceso al DOM — se comunica con las páginas mediante eventos y mensajes, nunca tocando document.

Registrar un service worker

El worker se registra desde el script normal de la página. Detecta primero el soporte y luego indícale el archivo al navegador:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/sw.js')
      .then((reg) => console.log('Scope:', reg.scope))
      .catch((err) => console.error('Fallo SW:', err));
  });
}

La ubicación del archivo importa: un worker en /sw.js controla todo el origen, mientras que uno en /app/sw.js solo controla las páginas bajo /app/. Esa frontera es su alcance (scope).

El ciclo de vida: install y activate

Un service worker tiene su propia vida, independiente de la página. Los dos eventos que más importan son install (se dispara una vez, ideal para precachear recursos) y activate (ideal para limpiar cachés antiguas):

const CACHE = 'site-v1';
const ASSETS = ['/', '/index.html', '/styles.css', '/app.js', '/offline.html'];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE).then((cache) => cache.addAll(ASSETS))
  );
});

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))
    )
  );
});

Llamar a event.waitUntil() le indica al navegador que no dé la fase por terminada hasta que tu promesa se resuelva — de lo contrario podría destruir el worker en plena tarea.

Una bahía de servidor con ranuras para discos duros y luces de estado
La Cache API guarda las respuestas en el dispositivo del usuario, no en un servidor como el de la foto — esa copia local es lo que permite cargar una página sin red.

Interceptar peticiones con fetch

El evento fetch es donde ocurre la magia. Tú decides qué devuelve cada petición. Una estrategia común y segura es caché primero con red de respaldo:

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      return cached || fetch(event.request);
    })
  );
});

Para mostrar una página sin conexión personalizada cuando fallan tanto la caché como la red, añade un catch:

self.addEventListener('fetch', (event) => {
  event.respondWith(
    fetch(event.request).catch(() => caches.match('/offline.html'))
  );
});

Las estrategias de caché de un vistazo

EstrategiaComportamientoIdeal para
Caché primeroServir la caché, si no la redRecursos estáticos (CSS, JS, fuentes)
Red primeroIntentar la red, si no la cachéHTML, datos de API que cambian
Stale-while-revalidateServir la caché, actualizarla en segundo planoAvatares, feeds, listas de contenido

Desplegar actualizaciones sin romper nada

Cuando modificas sw.js, el navegador detecta los nuevos bytes e instala el nuevo worker, pero queda en espera mientras alguna pestaña siga usando el antiguo. Eso evita que se ejecuten dos versiones a la vez. Para tomar el relevo antes, llama a self.skipWaiting() en install y a clients.claim() en activate — pero hazlo solo si tienes la certeza de que la nueva versión es compatible con las páginas ya abiertas.

Incrementa siempre el nombre de la caché (site-v1site-v2) cuando cambien los recursos. El manejador activate de arriba borra entonces la caché obsoleta, y los usuarios nunca obtienen una mezcla de archivos viejos y nuevos.

Ese es todo el bucle central: registrar, precachear en install, limpiar en activate y responder a las peticiones en fetch. Empieza con una estrategia de caché primero para los archivos estáticos y de red primero para tu HTML, y tendrás un sitio que carga al instante en las visitas repetidas y sobrevive a una caída de conexión.