Service Worker: guía práctica para PWA, caché inteligente y experiencias offline

Autor: Jeffry Chaves, Ing. en Sistemas – Diccionario Informático

Qué es un Service Worker y cuándo usarlo

Un Service Worker (SW) es un script que el navegador ejecuta en segundo plano, separado del hilo principal de la página. Funciona como un proxy de red programable: puede interceptar peticiones fetch, responder desde caché, actualizar recursos de forma inteligente y habilitar experiencias offline y de rendimiento percibido muy superiores.

Piensa en él como un portero con reglas claras: decide qué entra de la red, qué sale de tu caché y cuándo renovar el contenido. A diferencia de los Web Workers, los SW tienen ciclo de vida propio (no dependen de una pestaña concreta), persisten entre sesiones y disponen de APIs especiales como Cache Storage, Background Sync y Push.

Cuándo tiene sentido:

  • Tu web sirve muchos assets estáticos (CSS, JS, imágenes) y quieres reducir latencia.

  • Tienes rutas críticas (p. ej., /**) donde una copia offline evita pantallas en blanco.

  • Deseas prefetch de recursos posteriores (posterior navegación) para que todo “parezca instantáneo”.

  • Quieres resiliencia ante redes inestables (móvil, zonas con mala cobertura).

Requisitos clave:

  • HTTPS (o localhost en desarrollo).

  • Ubica sw.js en una ruta cuyo scope cubra lo que necesitas (por ejemplo, en raíz / si quieres abarcar todo el sitio).

  • Diseño “offline-first” en rutas HTML (fallback HTML + assets mínimos).

Diferencias con Web Workers y por qué actúa como “proxy de red”

  • Web Worker: acelera cálculos/CPU de tu página; no intercepta red ni vive fuera de la pestaña.

  • Service Worker: puede interceptar todas las peticiones de su scope, responder desde caché, sincronizar en segundo plano y recibir push. Es ideal para PWA.

Requisitos: HTTPS y localhost

  • En producción, sirve siempre bajo HTTPS y revisa cabeceras (por ejemplo, Service-Worker-Allowed si el archivo está en un subdirectorio y quieres ampliar el scope).

  • En desarrollo, funciona en http://localhost.


Ciclo de vida y actualización controlada

El ciclo de vida tiene tres grandes fases:

  1. install: el navegador descarga el SW y ejecuta la lógica de instalación (típicamente precache).

  2. waiting: si ya hay un SW activo, el nuevo queda “en espera” para no romper la sesión actual.

  3. activate: el nuevo SW toma el control (limpieza de cachés antiguas, migraciones).

La gestión fina de actualizaciones evita sustos al usuario (assets desincronizados, SPAs a medio cargar). Dos APIs clave:

  • self.skipWaiting(): hace que el SW recién instalado pase de waiting a active de inmediato.

  • clients.claim(): el SW activo toma control de todas las páginas bajo su scope sin esperar a que se recarguen.

Patrón recomendado (actualización con aviso):

  1. Durante install, precachea la nueva versión de assets (usa un CACHE_VERSION).

  2. No llames a skipWaiting() todavía: anuncia que hay una versión nueva (postMessage al cliente).

  3. Muestra un banner en la UI (“Hay una nueva versión. Actualizar”).

  4. Si el usuario acepta, envías un mensaje al SW para ejecutar skipWaiting() y luego recargas las páginas controladas.

Ejemplo simple de ciclo:

// sw.js
const VERSION = 'v4';
const PRECACHE = `precache-${VERSION}`;
const RUNTIME = `runtime-${VERSION}`;
const ASSETS = [
'/', '/index.html', '/styles.css', '/app.js', '/offline.html'
];
self.addEventListener(‘install’, (event) => {
event.waitUntil(
caches.open(PRECACHE).then((cache) => cache.addAll(ASSETS))
);
// Mantener en waiting hasta que el usuario acepte actualizar desde la UI.
});

self.addEventListener(‘activate’, (event) => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(keys
.filter(k => ![PRECACHE, RUNTIME].includes(k))
.map(k => caches.delete(k))
)
).then(() => self.clients.claim())
);
});

skipWaiting() y clients.claim(): actualización sin sorpresas

  • Úsalos de forma coordinada con la UI para evitar “mezcla” de assets viejos y nuevos.

  • Si tu app es estática y el riesgo es bajo, puedes activar skipWaiting() directamente tras precache (menor fricción).

Patrón “aviso de nueva versión” en la UI

En la página:

navigator.serviceWorker.addEventListener('message', (evt) => {
if (evt.data?.type === 'NEW_VERSION') {
// Mostrar banner de actualización
}
});
// Pregunta periódica al SW
navigator.serviceWorker.controller?.postMessage({ type: ‘CHECK_VERSION’ });

En el SW:

self.addEventListener('message', async (evt) => {
if (evt.data?.type === 'APPLY_UPDATE') {
await self.skipWaiting();
const all = await self.clients.matchAll({ type: 'window' });
all.forEach(c => c.navigate(c.url)); // recarga controlada
}
});

Registro y primer SW en 5 minutos

Registrar un SW es directo y reversible; si algo sale mal, lo desinstalas limpamente.

Dónde ubicar sw.js y definir el scope

  • En /sw.js para cubrir todo el sitio.

  • Si lo pones en /app/sw.js, por defecto solo controla /app/*. Puedes ampliar con la cabecera Service-Worker-Allowed: /.

navigator.serviceWorker.register() explicado (snippet incluido)

<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const reg = await navigator.serviceWorker.register('/sw.js', { scope: '/' });
console.log('SW registrado', reg.scope);
// Detectar actualización
reg.addEventListener(‘updatefound’, () => {
const newWorker = reg.installing;
newWorker?.addEventListener(‘statechange’, () => {
if (newWorker.state === ‘installed’ && navigator.serviceWorker.controller) {
// Hay una nueva versión en waiting: avisa a la UI
// postMessage o un evento custom en la app
}
});
});
} catch (e) {
console.error(‘Falló el registro del SW’, e);
}
});
}
</script>

Checklist de verificación (errores comunes)

  • ¿Estás en HTTPS (o localhost)?

  • ¿sw.js devuelve 200 y Content-Type: application/javascript?

  • ¿Scope correcto (raíz vs subcarpeta)?

  • ¿Assets precache listados sin typos y accesibles?

  • ¿Limpiando cachés antiguas en activate?

  • ¿Sin cachear rutas sensibles (/login, /api/auth)?

  • ¿Manejando fallback HTML para offline?


Intercepción de solicitudes y estrategias de caché

El evento fetch es el corazón del SW: decide de dónde responder y cuándo actualizar.

self.addEventListener('fetch', (event) => {
const req = event.request;
// Ejemplo: estrategia SWR para assets estáticos
if (req.destination === 'style' || req.destination === 'script' || req.destination === 'image') {
event.respondWith(staleWhileRevalidate(req));
}
});
async function staleWhileRevalidate(request) {
const cache = await caches.open(‘runtime-v4’);
const cached = await cache.match(request);
const fetchPromise = fetch(request).then((networkResp) => {
if (networkResp && networkResp.status === 200) {
cache.put(request, networkResp.clone());
}
return networkResp;
});
return cached || fetchPromise; // primero rápido, luego actualiza
}

Cache Storage/IndexedDB: qué guardar y cómo invalidar

  • Cache Storage para respuestas HTTP (assets, HTML, JSON de lectura).

  • IndexedDB para datos estructurados, colas de sincronización, flags de versión.

  • Invalidación: versiona los nombres de caché (runtime-v4) y limpia en activate. Evita “cache busting” por URL con hash + strategia duplicada.

Patrones frecuentes (tabla de decisión)

EstrategiaIdeal paraVentajasRiesgos / Notas
cache-firstImágenes, fuentes, vendor JS/CSS con hashVelocidad súper altaPuede servir viejo si no se renueva
network-firstHTML y JSON donde quieres frescuraDatos actualesLento sin red; requiere fallback
stale-while-revalidateAssets estáticos frecuentemente usadosCarga instantánea + actualización en backgroundNecesitas gestión de versiones
network-only/api/auth, pagos, endpoints críticosSeguridad / estado realSin beneficio offline
cache-onlyRecursos inmutables (fuentes auto-hosteadas)Cero latenciaAsegura política de actualización

Casos de uso que sí aportan valor

Offline real (assets críticos + rutas HTML)

  • Precarga shell de la app (index.html, app.js, styles.css) y un fallback offline.html.

  • Para SPAs, cachea rutas de navegación (p. ej., usar URL Pattern o lógica por Accept: text/html).

Prefetch predictivo y rendimiento percibido

  • Durante idle, el SW puede prefetchar rutas probables (siguiente página) para que la navegación sea instantánea.

  • Controla el peso: limita por dispositivo/red (usa NetworkInformation.effectiveType desde la página para decidir).

API mocking y fallbacks elegantes

  • Sin red, responde con una copia reciente desde caché o una respuesta simulada (para listas).

  • Marca visualmente que es “modo sin conexión” e intenta revalidar en background con Background Sync.


Push Notifications y Background Sync (visión general)

Qué permite cada API y consideraciones de permisos

  • Push: recibir notificaciones desde el servidor (vía push service) incluso con la app cerrada. Requiere permiso explícito del usuario y estrategia de valor (mensajes útiles, no spam).

  • Background Sync: reintenta peticiones fallidas cuando vuelve la conectividad (subidas de formularios, colas). Ideal para UX robusta.

Cuándo integrarlo (y cuándo no)

  • : apps de contenido relevante (actualizaciones útiles), mensajería, pedidos.

  • No: sitios informativos genéricos; si el valor es bajo, evitar pedir permiso (afecta confianza y SEO por métricas de abandono).


Seguridad, límites y gotchas

Qué NO puedes usar dentro del SW

  • Sin acceso al DOM, ni a window, ni a localStorage.

  • Usa cache, fetch, clients, registration, IndexedDB y APIs de eventos.

Coste de arranque del SW y bypass estratégico

  • El SW añade un pequeño overhead en el arranque. No interceptes todo si no aporta valor. Por ejemplo, deja pasar ciertas rutas estáticas directamente a la red si tu CDN ya ofrece buena latencia.

Depuración y limpieza de cachés antiguas

  • Usa Application > Service Workers y Application > Cache Storage en DevTools para inspeccionar, forzar updates y borrar.

  • Implementa limpieza agresiva en activate y registra métricas (cantidad de entradas, tamaño aproximado) si tu caso lo requiere.


FAQ y recursos para ir más lejos

Errores típicos en producción y cómo detectarlos

  • SW no se instala: revisa HTTPS, scope y tipo de contenido.

  • Assets 404 en precache: interrumpe install → nada se activa. Añade verificaciones y tests de build.

  • Pantalla en blanco offline: falta fallback HTML o rutas no cubiertas.

  • Usuario ve mezcla de versiones: habilita actualización controlada con banner + skipWaiting() bajo demanda.

Recursos oficiales y utilidades (MDN, Workbox, devtools)

  • Documentación de API (MDN) para detalles finos y compatibilidad.

  • Workbox (biblioteca de Google) ofrece estrategias de caché listas, routing declarativo y precache con manifest.


Conclusión

Un Service Worker bien diseñado es la pieza que convierte una web en app veloz, resiliente y usable sin conexión. La clave no es “cachearlo todo”, sino elegir la estrategia adecuada por tipo de recurso, cuidar el ciclo de vida y comunicar la actualización al usuario. Con el flujo de registro correcto, un precache mínimo y stale-while-revalidate para los assets adecuados, puedes reducir latencia, evitar errores por red y ofrecer una experiencia significativamente mejor.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *