⏱️ Lectura: 16 min
Apagá el wifi a la mitad de una sesión y algunas webs siguen respondiendo al instante: ese comportamiento no es magia del navegador, lo controla un Service Worker, un script que corre separado de la página y decide qué hacer con cada petición de red.
📑 En este artículo
- TL;DR
- Qué es un Service Worker y por qué importa
- Cómo funciona: el ciclo de vida completo
- Ejemplos prácticos: de cero a cache-first
- Cómo empezar: de un sitio estático a una PWA instalable
- Casos de uso reales
- Errores comunes y buenas prácticas
- Comparativa: qué estrategia de caché usar
- Profundizando: Background Sync, Push y Workbox
- Preguntas frecuentes
- Referencias
Es la pieza que convierte una web común en una Progressive Web App (PWA): la que permite abrir una app de noticias o el sitio que leíste ayer y ver contenido aunque el túnel del metro te deje sin señal. Esta guía explica el ciclo de vida completo de un service worker, las estrategias de caché que usan las PWA en producción y los errores que rompen una implementación en el primer despliegue.
TL;DR
- Vas a entender el ciclo de vida completo de un Service Worker: install, activate y fetch.
- Vas a poder registrar un Service Worker y cachear assets con la Cache API en minutos.
- Vas a distinguir 5 estrategias de caché (cache-first, network-first, stale-while-revalidate y más) y cuándo usar cada una.
- Vas a saber depurar un Service Worker en Chrome DevTools y forzar su actualización sin cerrar pestañas.
- Vas a construir un manifest.json básico para que tu web sea instalable como PWA.
- Vas a conocer los gotchas reales: scope, HTTPS obligatorio y el problema del worker zombie.
- Vas a comparar Service Workers contra AppCache (obsoleto) y contra Workbox como capa de abstracción.
Qué es un Service Worker y por qué importa
Un service worker es un script de JavaScript que el navegador ejecuta en un hilo aparte, sin acceso al DOM, a window ni a document. Se registra una sola vez desde tu página y después queda instalado en el navegador del usuario, activo incluso con la pestaña cerrada.
Su función principal es actuar como un proxy de red programable: intercepta cada fetch que hace la página y decide si responde desde la Cache API, si va a la red, o si combina ambas cosas. Esa capacidad es lo que hace posible el modo offline de una PWA, la carga instantánea de assets repetidos y las notificaciones push.
Antes del service worker, el único mecanismo de caché offline era AppCache, una API tan difícil de depurar que terminó deprecada. El Service Worker la reemplazó porque da control explícito, evento por evento, sobre qué se cachea y cuándo, en vez de una lista declarativa de todo o nada.
Correr en un hilo separado no es un detalle menor: significa que un service worker puede seguir vivo procesando una notificación push o una sincronización en segundo plano aunque el usuario haya cerrado todas las pestañas del sitio. Ningún otro script de la página tiene ese privilegio.
Cómo funciona: el ciclo de vida completo
Un service worker pasa por estados bien definidos, y entender esos estados es la diferencia entre una PWA que actualiza bien y una que sirve contenido viejo durante semanas.
Registro e instalación
Todo empieza cuando la página llama a navigator.serviceWorker.register(). El navegador descarga el archivo, lo compara byte a byte con la versión anterior (si existe) y, si cambió, dispara el evento install. Ese es el momento típico para precachear el app shell: el HTML, CSS y JS mínimos para que la interfaz cargue sola, sin depender de la red.
Activación y control
Después de instalarse, el worker queda en estado waiting hasta que ninguna pestaña use la versión anterior. En activate es el lugar correcto para borrar cachés obsoletos de versiones previas. Recién ahí el worker empieza a controlar las peticiones de red de las páginas abiertas.
flowchart TD
A["Registro: navigator.serviceWorker.register()"] --> B["Instalando (installing)"]
B --> C["Instalado, en espera (waiting)"]
C --> D["Activando (activating)"]
D --> E["Activado (activated)"]
E --> F["Controla la página y responde fetch"]
F --> G["Redundante (redundant)"]
El siguiente diagrama muestra qué pasa cada vez que la página pide un recurso una vez que el service worker ya está activo y controlando la pestaña:
sequenceDiagram
participant N as Navegador
participant SW as Service Worker
participant C as Cache Storage
participant R as Red
N->>SW: solicita index.html
SW->>C: busca en cache
alt Recurso en cache
C-->>SW: devuelve recurso
SW-->>N: responde desde cache
else No está en cache
SW->>R: pide el recurso a la red
R-->>SW: responde datos
SW->>C: guarda copia en cache
SW-->>N: responde con datos frescos
end
Ejemplos prácticos: de cero a cache-first
Vamos a construir un service worker real en tres pasos, cada uno agregando una capa de comportamiento sobre el anterior.
Paso 1: registrar el worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(function(reg) {
console.log('Service Worker registrado con scope:', reg.scope);
});
}
Este bloque va en tu HTML principal o en el bundle de arranque. Si el navegador no soporta serviceWorker, el if evita el error y la web sigue funcionando de forma normal, solo que sin capacidades offline.
Paso 2: cachear el app shell en install
const CACHE_NAME = 'programacion-app-v1';
const APP_SHELL = ['/', '/index.html', '/estilos.css', '/app.js'];
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME).then(function(cache) {
return cache.addAll(APP_SHELL);
})
);
self.skipWaiting();
});
Cuando este worker se instala, descarga y guarda en caché los cuatro archivos listados. event.waitUntil() le dice al navegador que no marque la instalación como completa hasta que cache.addAll() termine; si un solo archivo de la lista falla, toda la instalación falla.
Paso 3: responder desde caché con fetch
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(cached) {
return cached || fetch(event.request).then(function(response) {
return caches.open(CACHE_NAME).then(function(cache) {
cache.put(event.request, response.clone());
return response;
});
});
})
);
});
Con este evento, cada petición primero busca coincidencia en caché. Si existe, responde al instante sin tocar la red (estrategia cache-first). Si no existe, pide el recurso a la red, lo guarda para la próxima vez y recién ahí responde a la página.
Bonus: stale-while-revalidate
function staleWhileRevalidate(request) {
return caches.open(CACHE_NAME).then(function(cache) {
return cache.match(request).then(function(cached) {
const fetchPromise = fetch(request).then(function(response) {
cache.put(request, response.clone());
return response;
});
return cached || fetchPromise;
});
});
}
Esta variante responde de inmediato con lo que haya en caché, aunque esté desactualizado, y en paralelo pide la versión nueva a la red para la próxima visita. Es el equilibrio entre velocidad percibida y datos frescos, y es la estrategia que Workbox usa por defecto para CSS y JS.
Cómo empezar: de un sitio estático a una PWA instalable
Para que el navegador ofrezca instalar tu sitio como app necesitás tres piezas: HTTPS, un service worker registrado y un manifest.json válido enlazado con <link rel='manifest' href='/manifest.json'> en el head.
{
"name": "Mi PWA de Programación",
"short_name": "MiPWA",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#0b5fff",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}
El campo display: standalone es lo que hace que la app abra sin la barra de direcciones del navegador, como una app nativa. Los íconos de 192×192 y 512×512 son el mínimo que exige Chrome para mostrar el prompt de instalación.
Para probarlo local sin comprar un certificado: primero, serví el sitio con un servidor que soporte HTTPS local o simplemente usá localhost, que el navegador exime del requisito de HTTPS. Segundo, abrí Chrome DevTools, pestaña Application, sección Service Workers, y confirmá que aparece como activated and is running. Tercero, corré una auditoría con Lighthouse, integrado en DevTools, y elegí la categoría Progressive Web App para ver qué falta.
navigator.serviceWorker.getRegistrations().then(function(regs) {
regs.forEach(function(reg) {
console.log(reg.scope, reg.active ? reg.active.state : 'sin worker activo');
});
});
Este snippet, corrido desde la consola del navegador, lista cada service worker registrado, su scope y su estado. Si reg.active es null, el worker todavía está instalando o esperando, y no está sirviendo nada todavía.
💡 Tip: llamá aself.skipWaiting()en el evento install yself.clients.claim()en activate si querés que el nuevo service worker tome control de inmediato, sin esperar a que el usuario cierre todas las pestañas.
Casos de uso reales
El patrón se repite en distintos rubros, siempre con la misma lógica: responder rápido desde caché y actualizar en segundo plano.
- Sitios de noticias que precachean el app shell para que el usuario vea el header y la navegación al instante, aunque la conexión del celular esté fallando.
- Catálogos de e-commerce que cachean imágenes de producto ya vistas, para que volver atrás en el navegador no vuelva a descargar nada.
- Documentación técnica offline-first, donde el desarrollador quiere seguir leyendo la referencia de una API en un vuelo o un túnel.
- Apps de mapas ligeras que cachean los tiles de la zona ya visitada, para que el mapa no quede en blanco al perder señal.
- Notificaciones push: un service worker puede recibir un mensaje del servidor y mostrar una notificación del sistema operativo aunque la pestaña esté cerrada, usando la Push API.
Errores comunes y buenas prácticas
⚠️ Ojo: los service workers solo corren sobre HTTPS, con la excepción de localhost para desarrollo. Si tu sitio sirve por HTTP plano en producción, el navegador ignora el registro sin lanzar un error visible en consola.
📌 Nota: el scope de un service worker lo define la carpeta donde vive el archivo .js. Si registrás/sw.jsdesde la raíz, controla todo el sitio; si vive en/app/sw.js, solo controla/app/*.
El error más frecuente en producción es publicar una actualización y que los usuarios sigan viendo la versión vieja durante días. Pasa porque el worker nuevo queda en waiting hasta que se cierran todas las pestañas, y la mayoría de la gente nunca cierra el navegador del todo. La solución parcial es skipWaiting() más clients.claim(), aunque eso tiene su propio costo: una pestaña puede quedar a mitad de sesión con JS viejo y un service worker nuevo sirviendo assets distintos.
Otro error clásico es no versionar el nombre de la caché. Si CACHE_NAME queda fijo como app-cache para siempre, nunca vas a poder invalidar assets viejos sin borrar todo el Cache Storage a mano. La práctica estándar es sufijar con un número o hash de build, como app-cache-v3, y en activate recorrer caches.keys() para borrar cualquier caché que no coincida con la versión actual.
Cuando cacheás recursos de otro dominio sin CORS, por ejemplo una imagen de un CDN de terceros, la respuesta llega como opaque: el navegador la deja guardar en caché pero no te permite leer su tamaño ni su código de estado. Cachear demasiadas respuestas opacas puede agotar la cuota de almacenamiento sin que tu código se entere de que algo falló.
No todo sitio necesita un service worker. Un dashboard bancario que debe mostrar el saldo real en cada carga, o una landing page que un usuario visita una sola vez, no justifican la complejidad de invalidar caché. Como dice la broma clásica de la industria, atribuida al ingeniero Phil Karlton, hay solo dos cosas difíciles en ciencias de la computación: invalidar caché y nombrar variables. Un service worker mal versionado convierte la primera en un problema de producción real.
Comparativa: qué estrategia de caché usar
No hay una sola forma correcta de cachear con un service worker: la elección depende de qué tan crítico es que el dato esté actualizado.
| Estrategia | Cuándo usarla | Ventaja | Limitación |
|---|---|---|---|
| Cache First | Assets estáticos que casi no cambian (CSS, fuentes, íconos) | Respuesta instantánea, cero latencia de red | Si actualizás el archivo sin cambiar el nombre, el usuario ve la versión vieja |
| Network First | Contenido que debe estar fresco pero puede degradarse (feed de noticias) | Siempre intenta traer la última versión | Si la red es lenta, el usuario espera el timeout antes de ver el fallback |
| Stale While Revalidate | Contenido que cambia poco pero conviene mantener actualizado (avatares, listas) | Respuesta inmediata desde caché y actualización silenciosa en segundo plano | El usuario puede ver datos de una versión atrás por un instante |
| Network Only | Operaciones que no deben cachearse nunca (pagos, autenticación) | Garantiza datos siempre reales | No funciona sin conexión |
| Cache Only | Recursos empaquetados en el build que nunca cambian en runtime | Cero dependencia de la red | Requiere republicar el service worker para actualizar algo |
Vale la pena no confundir el service worker con un Web Worker común: este último sirve para mover cálculo pesado a otro hilo, pero no intercepta red ni sobrevive al cierre de la pestaña. Y frente a la vieja AppCache, la diferencia central es el control: AppCache cacheaba todo o nada según un archivo declarativo, mientras el service worker decide caso por caso, petición por petición, con código real.
Profundizando: Background Sync, Push y Workbox
Con el worker activo y cacheando bien, el siguiente nivel es la Background Sync API: permite que una acción que falló por falta de red, como enviar un formulario, quede en cola y se reintente sola en cuanto vuelva la conexión, sin que el usuario tenga que hacer nada. El soporte todavía es desigual: Chrome y Edge la implementan, Safari no.
La Push API combinada con el service worker es lo que permite notificaciones del sistema operativo desde una web, incluso con el navegador cerrado en algunos sistemas operativos. Requiere que el usuario dé permiso explícito y que el backend mande el mensaje a través de un servicio de push, ya que el navegador expone un endpoint único por dispositivo.
En producción, casi nadie escribe el fetch handler a mano como en los ejemplos de arriba. Workbox, la librería de Google, ofrece rutas declarativas: le decís qué expresión regular de URL usa qué estrategia, y ella genera el service worker completo.
flowchart LR
A["App"] --> B["Workbox Router"]
B --> C{"Tipo de request"}
C -->|"Imágenes"| D["CacheFirst"]
C -->|"API JSON"| E["NetworkFirst"]
C -->|"CSS y JS"| F["StaleWhileRevalidate"]
D --> G[("Cache Storage")]
E --> G
F --> G
Por dentro, Workbox sigue usando exactamente las mismas piezas que vimos antes: caches.open(), event.respondWith() y el ciclo install/activate. La librería no inventa una API nueva, empaqueta los mismos patrones en funciones reutilizables y le suma versionado automático de caché.
Otro detalle que conviene distinguir: Cache Storage guarda pares request-response completos, pensados para servir recursos de red tal cual. IndexedDB, en cambio, es la base de datos del navegador para guardar datos estructurados, como el contenido de un formulario pendiente de sincronizar. Un service worker completo suele usar las dos cosas a la vez, cada una para lo que le corresponde.
📖 Resumen en Telegram: Ver resumen
Tu próximo paso: creá un archivo sw.js vacío, registralo con el snippet del Paso 1, y confirmá en Chrome DevTools, pestaña Application, sección Service Workers, que aparece como activated antes de agregar una sola línea de caché.
Preguntas frecuentes
¿Un Service Worker puede acceder al DOM?
No. Corre en un hilo aparte, sin acceso a window ni document; solo puede interceptar peticiones de red, leer y escribir en Cache Storage e IndexedDB, y comunicarse con las pestañas abiertas vía postMessage.
¿Qué pasa si actualizo mi sw.js?
El navegador detecta el cambio byte a byte, instala el nuevo worker en paralelo y lo deja en estado waiting hasta que todas las pestañas con el worker viejo se cierren, salvo que uses skipWaiting() para forzar el reemplazo.
¿Service Worker y Web Worker son lo mismo?
No. Un Web Worker ejecuta cálculos pesados en otro hilo pero no puede interceptar red ni sobrevive al cierre de la pestaña. El Service Worker sí persiste y actúa como proxy de red incluso con la pestaña cerrada.
¿Funciona en Safari e iOS?
Safari soporta el registro básico y Cache Storage desde hace años, pero limita Background Sync y Push API, sobre todo fuera de apps agregadas a la pantalla de inicio del dispositivo.
¿Cuánto espacio puedo cachear?
Depende del navegador y el espacio libre en disco del dispositivo. Podés consultar la cuota disponible para tu origen llamando a navigator.storage.estimate() desde la consola.
Referencias
- MDN: Service Worker API: referencia completa de la interfaz, eventos y métodos.
- web.dev: Service Workers: guía oficial de Google sobre el ciclo de vida y patrones de caché.
- W3C: especificación de Service Workers: el documento técnico que define el estándar.
- Workbox: documentación de la librería que abstrae las estrategias de caché en producción.
📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.
Imagen destacada: Foto de Ferenc Almasi en Unsplash
0 Comentarios