Service Worker con Claude Code: caché, actualizaciones y offline
Guía práctica para implementar Service Worker con Claude Code: caché, ciclo de actualización, UX offline y ejemplos.
Un Service Worker es una de las piezas más útiles para PWA y experiencias offline, pero también una de las más fáciles de implementar mal. Si pides a Claude Code “añade caché” sin límites, la aplicación puede conservar HTML antiguo, guardar datos privados o no mostrar una nueva versión después del despliegue.
Piensa en el Service Worker como un pequeño proxy entre el navegador y tu servidor. Intercepta peticiones, decide si responde desde red, desde Cache API o desde una página offline, y se actualiza con su propio ciclo de vida. Esta guía reúne decisiones previas, un ejemplo copiable, invalidación de caché, UX offline, casos de uso y fallos concretos.
Para contrastar decisiones usa fuentes oficiales: MDN Service Worker API, guía de service workers en web.dev, guía de caché de web.dev y Chrome Workbox docs. Como contexto interno, consulta la guía PWA con Claude Code, estrategias de caché e IndexedDB con Claude Code.
Qué hace un Service Worker
Un Service Worker es JavaScript que vive fuera de la página. No puede tocar el DOM, así que no modifica botones, formularios ni estado de React directamente. Lo que sí puede hacer es interceptar peticiones elegibles y decidir si devuelve una respuesta de red, una respuesta cacheada o un fallback offline.
El script normal de la página termina cuando se cierra la pestaña. El Service Worker es event-driven: el navegador lo despierta para eventos install, activate, fetch, push y, según soporte, sincronización en segundo plano. Para empezar bien, céntrate en fetch, Cache API y ciclo de actualización.
sequenceDiagram
participant User as Usuario
participant Page as Página
participant SW as Service Worker
participant Cache as Cache API
participant Net as Servidor
User->>Page: Abre el sitio
Page->>SW: Registra /sw.js
Page->>SW: Lanza una petición fetch
SW->>Cache: Revisa caché
alt Hay caché
Cache-->>SW: Respuesta guardada
else No hay caché
SW->>Net: Pide respuesta nueva
Net-->>SW: Respuesta fresca
end
SW-->>Page: Respuesta para renderizar
La idea clave: no es magia de rendimiento, es control del tráfico. La calidad depende de qué guardas, cuándo lo borras y qué ve el usuario cuando falla la red.
Casos de uso realistas
| Caso | Por qué ayuda | Cuidado principal |
|---|---|---|
| Documentación o blog | Artículos, CSS, imágenes y fuentes cargan mejor al volver | HTML cacheado mucho tiempo oculta correcciones |
| Dashboard SaaS | La navegación y el esqueleto siguen visibles en mala red | No caches facturación, cuenta ni respuestas privadas |
| Formularios de campo | El usuario conserva borradores sin conexión | POST va a IndexedDB, no a Cache API |
| Catálogo ecommerce o media | Miniaturas y assets evitan descargas repetidas | Precio, stock e imágenes protegidas requieren frescura |
Masa probó este patrón en un sitio pequeño de aprendizaje. Cachear imágenes y fuentes mejoró claramente la segunda visita. Cachear el HTML de artículos con Cache First causó un problema visible: las correcciones tardaban en llegar. La instrucción correcta para Claude Code no es “cachea todo”, sino “cachea esto, durante este tiempo, y excluye esto”.
Prompt útil para Claude Code
Incluye prohibiciones y verificación. Este formato evita mucha ambigüedad.
Añade un Service Worker a la app Vite existente.
Requisitos:
- Coloca /sw.js en public y usa scope /
- Cachea solo assets estáticos con método GET
- Usa Network First para navegación HTML
- Devuelve /offline.html si la navegación falla offline
- No caches API, POST, páginas autenticadas ni otros orígenes
- Incluye fecha o versión en el nombre de caché
- Borra cachés antiguas durante activate
- Muestra aviso de recarga cuando el nuevo worker esté waiting
Verificación:
- Revisa Chrome DevTools > Application > Service Workers
- Activa Network Offline y confirma que aparece /offline.html
- Cambia CACHE_VERSION y confirma que se borran cachés antiguas
Las exclusiones son tan importantes como el código. Los bugs de caché en API y páginas privadas se convierten rápido en bugs de datos.
Implementación mínima copiable
Coloca estos cuatro archivos en un directorio vacío como sw-demo y levanta un servidor local. Service Worker requiere HTTPS o localhost; abrir el HTML directamente no basta.
python -m http.server 5173
Abre http://localhost:5173.
<!-- index.html -->
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Service Worker Demo</title>
<style>
body {
font-family: system-ui, sans-serif;
margin: 2rem;
line-height: 1.7;
}
button {
padding: 0.7rem 1rem;
}
</style>
</head>
<body>
<h1>Service Worker Demo</h1>
<p id="status">Esperando registro.</p>
<button type="button" onclick="location.reload()">Recargar</button>
<script src="/register-sw.js"></script>
</body>
</html>
// register-sw.js
const status = document.querySelector("#status");
let reloadRequested = false;
let updatePromptShown = false;
function setStatus(message) {
if (status) status.textContent = message;
}
function askToReload(worker) {
if (updatePromptShown) return;
updatePromptShown = true;
const ok = window.confirm("Hay una nueva versión. ¿Recargar ahora?");
if (ok) {
reloadRequested = true;
worker.postMessage({ type: "SKIP_WAITING" });
}
}
async function registerServiceWorker() {
if (!("serviceWorker" in navigator)) {
setStatus("Este navegador no soporta Service Worker.");
return;
}
try {
const registration = await navigator.serviceWorker.register("/sw.js", {
scope: "/",
});
setStatus(`Service Worker registrado: ${registration.scope}`);
if (registration.waiting && navigator.serviceWorker.controller) {
askToReload(registration.waiting);
}
registration.addEventListener("updatefound", () => {
const worker = registration.installing;
if (!worker) return;
worker.addEventListener("statechange", () => {
const hasOldController = Boolean(navigator.serviceWorker.controller);
if (worker.state === "installed" && hasOldController) {
askToReload(worker);
}
});
});
} catch (error) {
console.error(error);
setStatus("Falló el registro del Service Worker.");
}
}
navigator.serviceWorker?.addEventListener("controllerchange", () => {
if (!reloadRequested) return;
window.location.reload();
});
registerServiceWorker();
// sw.js
const CACHE_VERSION = "2026-06-02-v1";
const CACHE_PREFIX = "claude-sw-demo";
const CACHE_NAME = `${CACHE_PREFIX}-${CACHE_VERSION}`;
const APP_SHELL = [
"/",
"/index.html",
"/offline.html",
"/register-sw.js",
];
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL)),
);
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((names) =>
Promise.all(
names
.filter((name) => name.startsWith(CACHE_PREFIX))
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name)),
),
),
);
self.clients.claim();
});
self.addEventListener("message", (event) => {
if (event.data?.type === "SKIP_WAITING") {
self.skipWaiting();
}
});
self.addEventListener("fetch", (event) => {
const { request } = event;
if (request.method !== "GET") return;
const url = new URL(request.url);
if (url.origin !== self.location.origin) return;
if (request.mode === "navigate") {
event.respondWith(networkFirstNavigation(request));
return;
}
if (["style", "script", "font", "image"].includes(request.destination)) {
event.respondWith(staleWhileRevalidate(request));
}
});
async function networkFirstNavigation(request) {
const cache = await caches.open(CACHE_NAME);
try {
const response = await fetch(request);
if (response.ok) cache.put(request, response.clone());
return response;
} catch {
return (
(await cache.match(request)) ||
(await cache.match("/offline.html")) ||
new Response("Offline", { status: 503 })
);
}
}
async function staleWhileRevalidate(request) {
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(request);
const fetched = fetch(request)
.then((response) => {
if (response.ok) cache.put(request, response.clone());
return response;
})
.catch(() => cached || new Response("Offline", { status: 503 }));
return cached || fetched;
}
<!-- offline.html -->
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sin conexión</title>
</head>
<body>
<main>
<h1>Sin conexión</h1>
<p>Cuando vuelva la conexión, recarga esta página.</p>
<button type="button" onclick="location.reload()">Reintentar</button>
</main>
</body>
</html>
El ejemplo usa Network First para navegación y Stale While Revalidate para CSS, JavaScript, fuentes e imágenes. Network First consulta primero el servidor y solo cae al fallback si falla. Stale While Revalidate muestra caché enseguida y actualiza en segundo plano. No lo apliques sin pensar a noticias, precios, inventario o pantallas autenticadas.
Actualización e invalidación
El ciclo de actualización es el punto más delicado. Cuando cambia sw.js, el navegador instala un worker nuevo. Si una página antigua sigue abierta, ese worker puede quedar en estado waiting. El código de registro lo detecta, pregunta al usuario y envía SKIP_WAITING solo si acepta.
El worker llama entonces a self.skipWaiting(), activa, borra cachés antiguas y reclama clientes. Sin este flujo, los usuarios pueden seguir con un app-cache-v1 viejo después de que despliegues una corrección.
Usa nombres de caché con fecha, número de release o commit. Si tu build genera archivos con hash, sincroniza la lista de precache con el resultado del build. Cuando una lista manual empieza a doler, Workbox puede ayudar; aun así, Workbox no decide qué datos de negocio es seguro cachear.
UX offline
El soporte offline no termina al devolver algo desde Cache API. El usuario necesita saber si su trabajo está guardado, esperando sincronización o fallido. En formularios, no guardes POST en Cache API: guarda borradores o trabajos pendientes en IndexedDB y reintenta al volver online. Background Sync ayuda, pero su soporte varía, así que añade evento online y botón visible de reintento en flujos críticos.
Al pedirlo a Claude Code, incluye texto offline, botón de reintento, estado de borrador y mensajes de fallo. En una app de campo, separar “enviado”, “guardado en este dispositivo” y “falló la sincronización” reduce soporte y confusión.
Fallos comunes
El primero es el scope. Un worker en /app/sw.js controla /app/ por defecto, no todo el sitio. Para control global, colócalo en /sw.js y registra scope /.
El segundo es cache.addAll() con una URL 404. Si una sola entrada falla, falla todo el install. Después de que Claude Code añada archivos, revisa Application y Cache Storage en DevTools.
El tercero son datos privados. No caches /api/me, páginas de facturación, HTML de administración ni JSON específico del usuario sin una estrategia deliberada de borrado. La caché del navegador sigue siendo almacenamiento en el dispositivo del usuario.
El cuarto es omitir UX de actualización. Workers antiguos pueden conservar JavaScript y CSS viejos. Versiona cachés, borra en activate y deja que el usuario recargue cuando haya un worker waiting.
Por último, Service Worker no es almacenamiento permanente. El navegador puede expulsar cachés si falta espacio. Las respuestas opaque de otros orígenes son difíciles de medir. No puede tocar DOM. Solo funciona en HTTPS o localhost. No prometas “offline completo” sin probar el flujo real.
Resumen y CTA
Un Service Worker mejora revisitas, comportamiento offline y calidad PWA. La ruta segura es decidir propiedad de caché, ciclo de actualización, reglas de datos privados y pantallas offline antes de que Claude Code edite archivos.
ClaudeCodeLab ayuda con conversión PWA, diseño de caché, formularios offline, migración a Workbox y revisión de implementaciones con Claude Code. Si tu sitio necesita ser más rápido sin servir datos antiguos o privados, empieza por formación y consultoría Claude Code.
Probando esta configuración mínima en Chrome local, después de la primera carga aparece claude-sw-demo-2026-06-02-v1 en Application. Al cambiar Network a Offline y recargar, se muestra offline.html. Al modificar CACHE_VERSION, las cachés antiguas se eliminan durante activate, lo que la convierte en una buena base de verificación.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Escalera de permisos de Claude Code para ampliar acceso sin perder control
Pasa de read-only a ediciones limitadas, comandos de prueba y checks de deploy con menos riesgo.
Claude Code Small PR Proof Pack: cambios pequeños que sí se pueden revisar
Un paquete de prueba para PRs de Claude Code: diff, checks, URL pública, CTA y rollback.
Gate de revisión antes del commit con Claude Code
Cómo revisar con Claude Code antes del commit: diff, build, URL pública, Gumroad, consultoría, tests y archivos ajenos.