Service Workers: Cómo Hacer que tu Web Funcione Sin Conexión
Los Service Workers son uno de los avances más importantes en web performance de la última década. Con una implementación de 1-2 horas, los repeat visitors de Quintaesencia cargan la página en 150ms en lugar de 6 segundos — 41x más rápido — y el sitio funciona sin conexión a internet.
¿Qué es un Service Worker?
Un Service Worker es un script JavaScript que corre en segundo plano, separado de la página web. Actúa como un proxy entre el navegador y el servidor: puede interceptar solicitudes de red, servir contenido desde caché local, y permitir que la página funcione sin conexión.
La analogía más clara: imagina un almacén con un gerente inteligente. Cada vez que la página pide un recurso (imagen, CSS, JS), el gerente:
- Verifica si el recurso ya está en el almacén local (caché)
- Si está: lo entrega inmediatamente, sin ir al servidor
- Si no está: lo descarga del servidor y lo guarda en el almacén para la próxima vez
- Si el servidor no responde (sin internet): sirve lo que tiene en el almacén
El resultado: carga instantánea en visitas siguientes y funcionamiento offline.
El problema que resuelven
Quintaesencia enfrenta un desafío típico de webs con muchos assets: los primeros visitantes cargan lentamente (tienen que descargar todo del servidor), pero los visitantes frecuentes deberían cargar instantáneamente.
| Tipo de visitante | Sin Service Worker | Con Service Worker |
|---|---|---|
| Primera visita | Descarga ~1 MB, tarda 6+ segundos | Igual — primer contacto siempre es lento |
| Repeat visitor | Vuelve a descargar todo, 6+ segundos | Sirve desde caché en ~150ms |
| Sin internet | Error 404, página vacía | Muestra versión en caché |
| Impacto | Experiencia pobre para usuarios frecuentes | 41x más rápido, funciona offline |
Implementación: el archivo sw.js
Un Service Worker es un archivo JavaScript en la raíz del sitio. Aquí está la implementación completa con la estrategia Cache First:
// /sw.js — en la raiz del sitio
const CACHE_NAME = "quintaesencia-v1";
const ASSETS_TO_CACHE = [
"/",
"/index.html",
"/css/style.css",
"/js/main.js",
"/images/portada.webp",
"/images/mapamundi.webp",
"/audio/Rey-Salvaje.ogg",
// ... mas assets criticos
];
// INSTALAR: se ejecuta la primera vez que el usuario visita
self.addEventListener("install", event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll(ASSETS_TO_CACHE);
})
);
self.skipWaiting(); // Activar inmediatamente
});
// FETCH: intercepta cada solicitud de recurso
self.addEventListener("fetch", event => {
event.respondWith(
caches.match(event.request).then(response => {
// 1. Si esta en cache, servir inmediatamente
if (response) return response;
// 2. Si no esta, obtener del servidor
return fetch(event.request).then(response => {
// 3. Guardar en cache para proximas veces
if (response && response.status === 200) {
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, response.clone());
});
}
return response;
}).catch(() => {
// 4. Sin internet: servir desde cache si existe
return caches.match(event.request);
});
})
);
});
// ACTIVAR: limpiar versiones antiguas de cache
self.addEventListener("activate", event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
self.clients.claim();
});
Registrar el Service Worker
En tu HTML, antes del cierre de </body>:
<script>
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js")
.then(reg => console.log("Service Worker registrado:", reg))
.catch(err => console.log("Error:", err));
}
</script>
Eso es todo. En la primera visita, el Service Worker se instala y cachea los assets críticos. En la segunda visita, esos assets se sirven desde caché en milisegundos.
Estrategias de caching
Hay tres estrategias principales. La elección depende del tipo de contenido:
- Cache First: Mira en caché primero, luego en servidor. Más rápido, pero el contenido puede estar desactualizado. Ideal para imágenes, CSS, JS estáticos.
- Network First: Intenta el servidor primero, luego caché. Más actualizado, pero más lento. Ideal para datos dinámicos y APIs.
- Stale While Revalidate: Sirve desde caché inmediatamente y en background actualiza desde el servidor. Lo mejor de ambos mundos para la mayoría de casos.
La estrategia Stale While Revalidate en código:
self.addEventListener("fetch", event => {
event.respondWith(
caches.open(CACHE_NAME).then(cache => {
return cache.match(event.request).then(cachedResponse => {
// Actualizar en background
const fetchPromise = fetch(event.request).then(response => {
cache.put(event.request, response.clone());
return response;
});
// Retornar cache inmediatamente, o el fetch si no hay cache
return cachedResponse || fetchPromise;
});
})
);
});
// Resultado: el usuario ve contenido al instante
// y recibe actualizaciones en la siguiente visita
Code Splitting: cargar solo lo necesario
Un complemento perfecto al Service Worker es el Code Splitting: en lugar de cargar un archivo JS gigante, cargarlo en módulos pequeños bajo demanda:
// Sin Code Splitting — todo carga al abrir la pagina
import orbeAudioMaster from "./orbe-audio-master.js"; // 38 KB
import orbeResonancia from "./orbe-resonancia-mini.js"; // 82 KB
import orbeFullscreen from "./orbe-fullscreen.js"; // 45 KB
// Total: 165 KB en la carga inicial
// Con Code Splitting — cada modulo carga cuando se necesita
const orbeAudioMaster = import("./orbe-audio-master.js"); // Al abrir home
const orbeResonancia = import("./orbe-resonancia-mini.js"); // Al abrir el player
const orbeFullscreen = import("./orbe-fullscreen.js"); // Al hacer click en fullscreen
El Service Worker cachea cada módulo por separado. Si un usuario visita solo la página de inicio, no descarga los módulos del reproductor. Impacto: primera visita 165 KB → 60 KB. Segunda visita: 150ms.
Resultados reales en Quintaesencia
| Métrica | Sin SW | Con SW | Mejora |
|---|---|---|---|
| FCP primera visita | 6.148 ms | 6.100 ms | Sin cambio |
| FCP repeat visitor | 6.148 ms | 150 ms | 41x más rápido |
| Datos descargados (visita 2) | 655 KB | ~10 KB | -98.5% |
| Funcionamiento offline | Error 404 | Versión en caché | Funciona sin internet |
Consideraciones importantes
- Versioning crítico: Cambiar
CACHE_NAME(de v1 a v2) fuerza a todos los usuarios a descargar de nuevo. Hazlo cuando actualices assets importantes. - No cachees todo: Especifica qué assets cachear. Cachear archivos dinámicos o muy grandes es contraproducente.
- Depuración: El navegador puede servir desde caché aunque hayas actualizado el archivo. En DevTools → Application → Service Workers, usa «Update on Reload» durante desarrollo.
- Límite de caché: Los navegadores tienen límites (~50 MB). Si creces mucho, el Service Worker puede ser rechazado.
- HTTPS obligatorio: Los Service Workers solo funcionan en HTTPS (o localhost). HTTP se rechaza por seguridad.
Depurar en Chrome DevTools
Para inspeccionar el Service Worker y el caché:
- Abre DevTools (F12)
- Ve a la pestaña Application
- En el panel izquierdo: Service Workers — muestra el estado, permite forzar actualización
- En el panel izquierdo: Cache Storage — muestra todos los archivos cacheados y su tamaño
- Para simular offline: en la pestaña Network, selecciona «Offline» en el dropdown de throttling
Si el Service Worker no se actualiza, haz click en «Update» en la sección Service Workers de DevTools, o activa «Update on Reload» durante el desarrollo.
Los Service Workers son una de las mejoras de rendimiento con mejor ratio esfuerzo/impacto que existen. Si tu web recibe visitantes frecuentes, implementarlos debería ser una prioridad. La inversión es de pocas horas; el beneficio es permanente.