JavaScript
Service workers y modo offline
Explica cómo los service workers interceptan solicitudes, administran caché y habilitan experiencias offline con un ciclo de vida separado de la página.
- Última actualización
- Actualizada
- Nivel
- Profundización
JavaScript
Explica cómo los service workers interceptan solicitudes, administran caché y habilitan experiencias offline con un ciclo de vida separado de la página.
Un service worker es un worker especializado que el navegador puede ejecutar entre una aplicación web y la red. Puede responder a solicitudes, usar Cache Storage y recibir eventos incluso cuando ninguna página concreta está ejecutando su script principal.
page
↓ request
service worker
├─ cache
├─ network
└─ generated responseNormalmente necesita:
localhost suele recibir una excepción para desarrollo.
if ("serviceWorker" in navigator) {
const registration = await navigator.serviceWorker.register(
"/service-worker.js",
{
type: "module",
},
);
console.log(registration.scope);
}El scope determina qué documentos y solicitudes puede controlar. La ubicación del archivo y headers del servidor influyen en ese límite.
La página que registra por primera vez puede no estar controlada hasta una navegación posterior.
navigator.serviceWorker.controller;Puede ser null aunque el registro exista.
parsed
↓
installing
↓
installed / waiting
↓
activating
↓
activatedUna nueva versión puede permanecer esperando mientras una versión anterior todavía controla clientes.
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open("app-shell-v1")
.then((cache) => cache.addAll([
"/",
"/styles.css",
"/app.js",
])),
);
});waitUntil extiende la vida del evento hasta que la promesa termina. Si falla una precarga esencial, la instalación puede fallar.
No precargues toda la aplicación sin considerar tamaño, cambios y recursos opcionales.
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((names) =>
Promise.all(
names
.filter((name) =>
name.startsWith("app-shell-") &&
name !== "app-shell-v2"
)
.map((name) => caches.delete(name)),
),
),
);
});La limpieza debe limitarse a caches que pertenecen a este service worker. No borres cualquier nombre del origen.
await self.clients.claim();Durante activación puede permitir que la versión nueva controle clientes abiertos sin esperar otra navegación. Cambiar controlador a mitad de una sesión puede producir incompatibilidades; diseña la actualización.
self.skipWaiting();Pide que la versión instalada deje de esperar. No debe aplicarse automáticamente si la nueva versión es incompatible con páginas abiertas o datos actuales.
self.addEventListener("fetch", (event) => {
event.respondWith(
fetch(event.request),
);
});respondWith define la promesa de Response para esa solicitud.
No interceptes sin necesidad métodos o destinos que tu estrategia no comprende.
const cache = await caches.open("assets-v1");
await cache.put(request, response);
const match = await cache.match(request);Cache Storage guarda pares Request-Response. No es la misma cache HTTP ni IndexedDB.
const response = await fetch(request);
const copy = response.clone();
await cache.put(request, copy);
return response;El body es un stream consumible. La clonación crea otra rama antes de almacenarlo y devolverlo.
async function cacheFirst(request) {
const cached = await caches.match(request);
if (cached) return cached;
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open("assets-v1");
await cache.put(request, response.clone());
}
return response;
}Aporta para assets versionados o poco cambiantes.
async function networkFirst(request) {
try {
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open("data-v1");
await cache.put(request, response.clone());
}
return response;
} catch {
const cached = await caches.match(request);
if (cached) return cached;
throw new Error("Network and cache unavailable");
}
}Puede servir datos antiguos. La UI debería saber si un recurso es stale cuando eso importa.
responder cache inmediatamente
+
actualizar cache en segundo planoReduce latencia, pero permite datos antiguos y necesita controlar fallos de actualización.
Evita guardar sin política:
La cache no interpreta autenticación ni invalida automáticamente por logout.
La coincidencia incluye propiedades del Request y reglas de Cache API. Variantes por headers, query strings o idioma pueden necesitar una clave explícita.
No ignores parámetros solo para aumentar hits si cambian el contenido.
Una experiencia offline necesita decidir:
Cachear el app shell no convierte automáticamente una app en offline-first.
Un service worker puede recibir ciertos eventos de sincronización en navegadores compatibles.
self.addEventListener("sync", (event) => {
if (event.tag === "send-pending-orders") {
event.waitUntil(sendPendingOrders());
}
});El soporte no es universal y el navegador decide cuándo ejecutar. No lo uses como garantía temporal exacta.
self.addEventListener("push", (event) => {
const payload = event.data?.json();
event.waitUntil(
self.registration.showNotification(
payload.title,
{
body: payload.body,
},
),
);
});Requiere permisos, suscripción y un servidor de push. No envíes datos sensibles sin considerar cifrado y visibilidad en pantalla bloqueada.
Service worker:
const clients = await self.clients.matchAll();
for (const client of clients) {
client.postMessage({
type: "UPDATE_AVAILABLE",
});
}Página:
navigator.serviceWorker.addEventListener(
"message",
(event) => {
const message = parseMessage(event.data);
},
);Variables globales del service worker pueden desaparecer cuando el navegador detiene el worker.
let pendingItems = [];No confíes en memoria global para datos persistentes. Usa IndexedDB o Cache Storage según el contrato.
Service workers no tienen acceso a window.localStorage. Es síncrono y pertenece al contexto de Window. IndexedDB es la alternativa estructurada habitual.
El navegador comprueba actualizaciones periódicamente y aplica reglas de cache al script del service worker.
Un error en la estrategia puede dejar usuarios con recursos incompatibles. Versiona caches, prueba actualizaciones y ofrece una experiencia de reload controlada.
Workbox proporciona estrategias, precaching, routing y herramientas de build.
Ayuda a evitar implementaciones repetidas, pero no decide por ti:
const CACHE_NAME = "shell-v1";
const OFFLINE_URL = "/offline.html";
self.addEventListener("fetch", (event) => {
if (
event.request.mode !== "navigate"
) {
return;
}
event.respondWith(
fetch(event.request).catch(async () => {
const cache = await caches.open(CACHE_NAME);
return cache.match(OFFLINE_URL);
}),
);
});El fallback se limita a navegaciones y no finge respuestas de API.
¿Por qué guardar datos pendientes en una variable global del service worker es inseguro?
Porque el navegador puede detener el worker cuando no hay eventos y crear otra instancia después. La variable puede desaparecer sin aviso.
Memoria, reachability y garbage collection inicia el cierre sobre funcionamiento interno y rendimiento.