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
fetchentrante, unpush, unsync) 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
localhostdurante el desarrollo), porque un script que puede reescribir cada respuesta es una superficie de ataque seria.
¿Por qué usar Service Workers?
| Beneficio | Lo que te ofrece |
|---|---|
| Soporte offline | Almacena en caché el shell de la aplicación y los recursos críticos para que la app cargue sin conexión de red. |
| Rendimiento | Las visitas repetidas se sirven desde una caché local, eliminando viajes de ida y vuelta y reduciendo los tiempos de carga. |
| Sincronización en segundo plano | Difiere las solicitudes fallidas (p. ej., un comentario publicado) y las reintenta automáticamente cuando se restaura la conectividad. |
| Notificaciones push | Recibe y muestra mensajes desde un servidor incluso cuando no hay ninguna pestaña abierta. |
| Control total de solicitudes | Decide 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?".
- Registrar — la página llama a
navigator.serviceWorker.register(). El navegador descarga el script. - Instalar — el evento
installse 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. - 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(). - Activar — el evento
activatese dispara. Aquí es donde limpias las cachés de versiones anteriores. - Control / Fetch — una vez activo, el worker intercepta los eventos
fetchde las páginas dentro de su scope.
register → install → (waiting) → activate → fetch / push / sync ...Dos métodos dirigen este flujo:
self.skipWaiting()(eninstall) indica al nuevo worker que se active de inmediato en lugar de esperar.self.clients.claim()(enactivate) 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.jscontrola/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 eventofetch— devuelve unaResponse(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 conclients.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.
| Estrategia | Cómo funciona | Ideal para |
|---|---|---|
| Cache first | Devuelve 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 first | Intenta la red; recurre a la caché si falla. | Contenido que se actualiza con frecuencia (respuestas de API, feeds de noticias). |
| Stale-while-revalidate | Sirve 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
Responsesolo se puede leer una vez, por eso debes usarclone()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:
- Incrementa
CACHE_VERSION(p. ej.'v1'→'v2') cada vez que cambien los recursos en caché. - El nuevo
installescribe los recursos en la nueva caché. - El nuevo
activateelimina 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
onlineyofflineen el objetowindowy 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:
- Promise y async/await — toda la API de Service Worker está basada en Promises.
- Fetch API — el mismo
fetch()que interceptas en el worker. - Storage API y localStorage & sessionStorage — dónde persistir los datos que gestiona el worker.
- Event loop: microtasks and macrotasks — cómo el worker programa su trabajo orientado a eventos.