W3docs

Service Workers

Aprende Service Workers en JavaScript: ciclo de vida, registro, estrategias de caché, soporte offline y cómo publicar actualizaciones con versiones de caché.

Service Workers: Creando Aplicaciones Web Offline-First Potentes

Un Service Worker es un script que el navegador ejecuta en segundo plano, separado de tu página web, sin acceso directo al DOM. Actúa como un proxy programable entre tu aplicación web y la red: cada solicitud que realiza la página puede ser interceptada, inspeccionada, servida desde una caché o reescrita antes de llegar al servidor.

Esta única capacidad desbloquea las funciones que los usuarios ahora esperan de las aplicaciones web modernas: funcionamiento offline, cargas repetidas casi instantáneas, sincronización de datos en segundo plano y notificaciones push. Los Service Workers son el motor detrás de las Progressive Web Apps (PWAs).

Este capítulo explica qué son los Service Workers, el ciclo de vida por el que pasan, cómo registrar uno, las estrategias de caché más comunes y cómo publicar actualizaciones sin servir archivos obsoletos. La API de Service Worker está construida completamente sobre Promises, por lo que un buen dominio de async/await y la Fetch API será de gran ayuda.

¿Qué es un Service Worker?

Un Service Worker es un tipo de web worker: un archivo JavaScript que se ejecuta en su propio hilo, independiente de la página que lo registró. Como se ejecuta fuera del hilo principal, no puede bloquear tu interfaz de usuario, pero tampoco puede acceder al DOM — se comunica con las páginas a través de eventos y mensajes.

Características clave que lo distinguen de un script de página normal:

  • Es orientado a eventos. El navegador lo inicia cuando hay trabajo por hacer (un fetch entrante, un push, un sync) y puede terminarlo cuando está inactivo. Nunca asumas que el estado global persiste entre eventos.
  • Tiene un ciclo de vida. Un Service Worker se instala, se activa y solo entonces controla las páginas. Las actualizaciones siguen reglas estrictas para que los usuarios nunca reciban una aplicación a medio actualizar.
  • Tiene un alcance definido. Un worker solo puede interceptar solicitudes bajo su scope — por defecto el directorio donde reside el script.
  • Requiere un contexto seguro. Los Service Workers solo se ejecutan sobre HTTPS (o localhost durante el desarrollo), porque un script que puede reescribir cada respuesta es una superficie de ataque seria.

¿Por qué usar Service Workers?

BeneficioLo que te ofrece
Soporte offlineAlmacena en caché el shell de la aplicación y los recursos críticos para que la app cargue sin conexión de red.
RendimientoLas visitas repetidas se sirven desde una caché local, eliminando viajes de ida y vuelta y reduciendo los tiempos de carga.
Sincronización en segundo planoDifiere las solicitudes fallidas (p. ej., un comentario publicado) y las reintenta automáticamente cuando se restaura la conectividad.
Notificaciones pushRecibe y muestra mensajes desde un servidor incluso cuando no hay ninguna pestaña abierta.
Control total de solicitudesDecide por cada solicitud si usar la caché, la red o lógica personalizada.

El ciclo de vida del Service Worker

Un Service Worker pasa por un conjunto bien definido de estados. Entenderlos es lo más importante para evitar los errores de "¿por qué sigue ejecutándose mi código antiguo?".

  1. Registrar — la página llama a navigator.serviceWorker.register(). El navegador descarga el script.
  2. Instalar — el evento install se dispara una vez por versión del worker. Aquí es donde se pre-almacenan en caché los archivos que la aplicación necesita para funcionar offline.
  3. Esperar — si un worker antiguo todavía controla páginas abiertas, el nuevo worker espera. No se activará hasta que se cierren todas las páginas controladas, a menos que llames a self.skipWaiting().
  4. Activar — el evento activate se dispara. Aquí es donde limpias las cachés de versiones anteriores.
  5. Control / Fetch — una vez activo, el worker intercepta los eventos fetch de las páginas dentro de su scope.
register → install → (waiting) → activate → fetch / push / sync ...

Dos métodos dirigen este flujo:

  • self.skipWaiting() (en install) indica al nuevo worker que se active de inmediato en lugar de esperar.
  • self.clients.claim() (en activate) permite que el worker activo tome el control de las páginas que ya están abiertas, en lugar de controlar solo las páginas cargadas después de la activación.

Por qué existe la fase de espera: garantiza que una sola versión de tu código controle una página durante toda su vida útil, para que nunca mezcles HTML antiguo con scripts recién cacheados. Usa skipWaiting() de forma deliberada, porque puede cambiar el worker de control durante una sesión de usuario activa.

Restricciones a tener en cuenta

  • Solo HTTPS o localhost. Las páginas mixtas/inseguras no pueden registrar un worker.
  • El scope limita la intercepción. Un worker en /app/sw.js controla /app/ y sus subdirectorios — no todo el origen. Coloca el script en la raíz del sitio para controlarlo todo.
  • Sin acceso al DOM. Actualiza la página enviando mensajes o haciendo que la página lea desde las cachés.
  • El worker puede ser terminado en cualquier momento. Almacena todo lo que deba persistir en el Cache Storage, IndexedDB o Web Storage — no en variables globales del worker.

Paso 1 — Registrar el Service Worker

En el JavaScript de tu página, registra el script con navigator.serviceWorker.register(). Siempre detecta la función primero y registra después de que la página cargue para que el worker no compita con el primer renderizado:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/sw.js')
      .then((registration) => {
        console.log('Service Worker registered, scope:', registration.scope);
      })
      .catch((error) => {
        console.error('Service Worker registration failed:', error);
      });
  });
}

La llamada a register() devuelve una Promise que se resuelve con un ServiceWorkerRegistration. Su scope te indica qué URLs controla este worker.

Paso 2 — Escribir el script del Service Worker

Crea un archivo separado (aquí, sw.js) para el worker en sí. Dentro de él manejas los eventos del ciclo de vida y decides cómo se sirven las solicitudes. El ejemplo a continuación pre-almacena en caché un shell de aplicación durante la instalación, limpia cachés antiguas en la activación y sirve una estrategia cache-first con un fallback offline:

const CACHE_VERSION = 'v1';
const PRECACHE_URLS = ['/', '/index.html', '/styles.css', '/offline.html'];

// Install: pre-cache the app shell.
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_VERSION).then((cache) => cache.addAll(PRECACHE_URLS))
  );
  self.skipWaiting(); // activate this version immediately
});

// Activate: remove caches from previous versions.
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches
      .keys()
      .then((keys) =>
        Promise.all(
          keys
            .filter((key) => key !== CACHE_VERSION)
            .map((key) => caches.delete(key))
        )
      )
      .then(() => self.clients.claim()) // take control of open pages
  );
});

// Fetch: serve from cache, fall back to network, then to the offline page.
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      return (
        cached ||
        fetch(event.request).catch(() => caches.match('/offline.html'))
      );
    })
  );
});

Algunos aspectos que vale la pena destacar:

  • event.waitUntil(promise) mantiene el worker activo hasta que la Promise se resuelva, para que el navegador no lo termine a mitad de la instalación o la activación.
  • event.respondWith(promise) es cómo respondes a un evento fetch — devuelve una Response (desde la caché) o una Promise que se resuelve en una.
  • self.skipWaiting() fuerza a la nueva versión a activarse sin esperar a que se cierren las páginas antiguas. Combinado con clients.claim(), el nuevo worker toma el control de inmediato. Conveniente en desarrollo; úsalo con cuidado en producción, porque cambiar el worker de control a mitad de una sesión puede interrumpir a los usuarios activos.

Paso 3 — El worker toma el control

Tras completar la instalación y la activación, el worker controla las páginas dentro de su scope y su manejador fetch intercepta sus solicitudes. Ten en cuenta que la primera carga de una página no pasa por el worker — el worker se está instalando durante esa carga. A partir de la segunda carga, las solicitudes fluyen a través de tu manejador fetch.

Estrategias de caché comunes

No existe una única estrategia de caché "correcta" — elige una por tipo de recurso según la frescura que deben tener los datos.

EstrategiaCómo funcionaIdeal para
Cache firstDevuelve la copia en caché; solo accede a la red si no hay coincidencia.Recursos estáticos que cambian raramente (CSS, fuentes, el shell de la app).
Network firstIntenta la red; recurre a la caché si falla.Contenido que se actualiza con frecuencia (respuestas de API, feeds de noticias).
Stale-while-revalidateSirve la copia en caché de inmediato, luego obtiene una copia actualizada en segundo plano para la próxima vez.Recursos donde la velocidad importa más que la frescura perfecta (avatares, miniaturas).

Un manejador network-first se ve así:

self.addEventListener('fetch', (event) => {
  event.respondWith(
    fetch(event.request)
      .then((response) => {
        // Cache a copy for offline use, then return the fresh response.
        const copy = response.clone();
        caches.open('v1').then((cache) => cache.put(event.request, copy));
        return response;
      })
      .catch(() => caches.match(event.request))
  );
});

Consejo: El cuerpo de una Response solo se puede leer una vez, por eso debes usar clone() antes de almacenarla en caché y devolverla.

Actualizar un Service Worker

Cuando cambias sw.js, el navegador detecta la diferencia de bytes, descarga el nuevo archivo y ejecuta su evento install. El nuevo worker espera entonces (a menos que llames a skipWaiting()). El patrón de versiones de caché anterior es lo que mantiene las actualizaciones limpias:

  1. Incrementa CACHE_VERSION (p. ej. 'v1''v2') cada vez que cambien los recursos en caché.
  2. El nuevo install escribe los recursos en la nueva caché.
  3. El nuevo activate elimina toda caché cuya clave no sea la versión actual, expulsando los archivos obsoletos.

Esto garantiza que los usuarios nunca reciban una mezcla de recursos antiguos y nuevos después de un despliegue.

Ejemplo del Mundo Real: Notificaciones de Estado de Conectividad

Este ejemplo demuestra una función utilizada habitualmente en muchos sitios y aplicaciones web modernas, como servicios de streaming como Netflix o aplicaciones en la nube como Google Docs, para informar a los usuarios sobre su estado de conectividad. Al notificar a los usuarios cuando están offline, estas plataformas mejoran la experiencia de usuario asegurándose de que son conscientes de posibles problemas con la sincronización de datos o las interrupciones de streaming. Este ejemplo se centra en la integración de la interfaz de usuario en el hilo principal, mientras que el script del Service Worker es el mismo que en el ejemplo anterior.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Connectivity Notifier</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        text-align: center;
        margin-top: 50px;
      }
      #status {
        padding: 10px;
        border-radius: 5px;
        color: #fff;
        font-size: 24px;
      }
      .online {
        background-color: #4caf50;
        animation: blinker 1s linear infinite;
      }
      .offline {
        background-color: #f44336;
        animation: blinker 1s linear infinite;
      }
      @keyframes blinker {
        50% {
          opacity: 0.5;
        }
      }
    </style>
  </head>
  <body>
    <h1>Connectivity Notifier</h1>
    <p id="status" class="offline">Checking connectivity...</p>

    <script>
      if ("serviceWorker" in navigator) {
        navigator.serviceWorker.register("sw.js").then(function () {
          console.log("Service Worker Registered");
        });

        window.addEventListener('online', () => {
          const statusElement = document.getElementById("status");
          statusElement.textContent = "Online";
          statusElement.className = "online";
        });

        window.addEventListener('offline', () => {
          const statusElement = document.getElementById("status");
          statusElement.textContent = "Offline";
          statusElement.className = "offline";
        });
      }
    </script>
  </body>
</html>

Explicación:

  • Verificación de conectividad: La página principal escucha los eventos online y offline en el objeto window y actualiza la interfaz de usuario de inmediato, evitando el enfoque poco fiable del polling.
  • Retroalimentación al usuario: La página muestra el estado de conectividad actual, ayudando a los usuarios a entender cómo integrar capacidades en segundo plano con una interfaz reactiva.
  • Limpieza de código: Se eliminó el listener muerto navigator.serviceWorker.onmessage, ya que el script del Service Worker no envía ningún mensaje.

Conclusión

Los Service Workers convierten el navegador en un proxy de red programable, haciendo posible crear aplicaciones que son rápidas, resistentes y utilizables offline. Las claves son entender el ciclo de vida (install → wait → activate → fetch), elegir una estrategia de caché que se adapte a cada recurso y usar el versionado de caché para que las actualizaciones se publiquen limpiamente.

Para profundizar en los bloques de construcción que usan los Service Workers, consulta:

Práctica

Práctica
¿Cuáles son las características y funcionalidades clave de los JavaScript Service Workers?
¿Cuáles son las características y funcionalidades clave de los JavaScript Service Workers?
Was this page helpful?