Service Workers en JavaScript: Cache API, estrategias offline y notificaciones push

Un Service Worker es un script JavaScript que el navegador ejecuta en segundo plano, separado de la página, y que puede interceptar todas las peticiones de red que hace tu aplicación. Esta capacidad de actuar como un proxy programable es la base de las Progressive Web Apps (PWA): permite que tu app funcione sin conexión, cargue al instante con datos en caché y envíe notificaciones push incluso cuando la pestaña está cerrada.

Registro y ciclo de vida del Service Worker

El Service Worker tiene tres fases principales: instalación, activación y fetch. La instalación ocurre cuando el navegador descarga el SW por primera vez o detecta que ha cambiado.

// main.js — registrar el SW
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js').then((registro) => {
    console.log('SW registrado con scope:', registro.scope);
  });
}

// sw.js
const CACHE_VERSION = 'v2';
const ASSETS = ['/', '/estilos.css', '/app.js', '/logo.png'];

self.addEventListener('install', (e) => {
  e.waitUntil(
    caches.open(CACHE_VERSION).then(cache => cache.addAll(ASSETS))
  );
  self.skipWaiting(); // activar inmediatamente sin esperar a que cierren las pestañas
});

self.addEventListener('activate', (e) => {
  e.waitUntil(
    caches.keys().then(claves =>
      Promise.all(
        claves.filter(k => k !== CACHE_VERSION).map(k => caches.delete(k))
      )
    )
  );
  self.clients.claim(); // tomar control de páginas ya abiertas
});

Estrategia Cache First

Cache First sirve el recurso desde caché si está disponible y solo va a la red si no lo tiene. Es ideal para assets estáticos con hash en el nombre que no cambian.

self.addEventListener('fetch', (e) => {
  if (e.request.method !== 'GET') return; // solo cachear GET

  e.respondWith(
    caches.match(e.request).then((cached) => {
      if (cached) return cached;
      return fetch(e.request).then((res) => {
        const copia = res.clone();
        caches.open(CACHE_VERSION).then(c => c.put(e.request, copia));
        return res;
      });
    })
  );
});

Estrategia Stale While Revalidate

Stale While Revalidate sirve desde caché de inmediato (para velocidad) y al mismo tiempo actualiza la caché en segundo plano. Perfecta para contenido que cambia ocasionalmente.

self.addEventListener('fetch', (e) => {
  e.respondWith(
    caches.open(CACHE_VERSION).then(async (cache) => {
      const cached = await cache.match(e.request);
      const peticionRed = fetch(e.request).then((res) => {
        cache.put(e.request, res.clone()); // actualizar en background
        return res;
      });
      return cached || peticionRed; // sirve caché si existe, si no espera red
    })
  );
});

Actualizar el SW sin dejar pestañas obsoletas

Cuando publicas una nueva versión del SW, el navegador lo detecta pero espera a que se cierren todas las pestañas con la versión anterior. Puedes forzar la actualización enviando un mensaje desde el SW y recargando la página:

// sw.js — en activate, avisar a los clientes
self.addEventListener('activate', (e) => {
  e.waitUntil(
    self.clients.matchAll({ type: 'window' }).then((clientes) => {
      clientes.forEach(c => c.postMessage({ tipo: 'sw-actualizado' }));
    })
  );
  self.clients.claim();
});

// main.js — reaccionar al mensaje
navigator.serviceWorker.addEventListener('message', (e) => {
  if (e.data?.tipo === 'sw-actualizado') {
    if (confirm('Nueva versión disponible. ¿Recargar?')) {
      window.location.reload();
    }
  }
});

Notificaciones push con VAPID

Las notificaciones push requieren que el servidor envíe un mensaje cifrado al servicio push del navegador usando el protocolo Web Push con claves VAPID. En el cliente, primero se pide permiso y se obtiene una suscripción:

// main.js
async function suscribirse() {
  const permiso = await Notification.requestPermission();
  if (permiso !== 'granted') return;

  const registro = await navigator.serviceWorker.ready;
  const suscripcion = await registro.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
  });

  // Enviar la suscripción al servidor
  await fetch('/api/push/suscribir', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(suscripcion),
  });
}

// sw.js — recibir y mostrar la notificación
self.addEventListener('push', (e) => {
  const datos = e.data?.json() ?? {};
  e.waitUntil(
    self.registration.showNotification(datos.titulo ?? 'Notificación', {
      body: datos.cuerpo,
      icon: '/icon-192.png',
      badge: '/badge-72.png',
      data: { url: datos.url },
    })
  );
});

self.addEventListener('notificationclick', (e) => {
  e.notification.close();
  e.waitUntil(clients.openWindow(e.notification.data.url));
});

El servidor necesita la clave privada VAPID para cifrar el mensaje. Librerías como web-push para Node.js lo gestionan automáticamente. Las notificaciones push llegan incluso cuando la página está cerrada, siempre que el navegador esté abierto y el usuario no haya bloqueado las notificaciones del sitio.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP