wandres.dev
SERVICE WORKERS EN DEVTOOLS · Depurar el SW

Forzar la actualización: skipWaiting, claim y actualizar al recargar

Las tres formas de saltarse la espera, qué garantía rompe cada una, la casilla que hace usable el desarrollo, y el patrón de actualización con consentimiento del usuario.

⏱ 18 min

La fase de espera protege a los usuarios y estorba enormemente durante el desarrollo, donde quieres ver tu cambio ahora y no cuando cierres todas las pestañas. Hay una casilla del panel que resuelve el problema en desarrollo y dos llamadas de la API que lo resuelven en producción a costa de romper una garantía. Saber cuál usar en cada contexto y qué se paga por cada una es lo que separa una actualización controlada de un usuario con media aplicación de cada versión.

🎯 Al terminar esta lección sabrás
  • Usar la casilla de actualización al recargar durante el desarrollo.
  • Explicar qué hacen exactamente las dos llamadas que saltan la espera.
  • Identificar la garantía que se rompe al saltarse la espera y sus consecuencias.
  • Implementar el patrón de actualización con aviso al usuario.

En desarrollo: la casilla

La sección de service workers tiene una casilla que fuerza la actualización del worker en cada recarga. Con ella marcada, el ciclo de desarrollo vuelve a ser el de siempre: cambias el código, recargas, ves el cambio.

Lo que hace es tres cosas a la vez: descarga el script en cada recarga, se salta la espera automáticamente, y activa el nuevo inmediatamente.

Marcarla debería ser lo primero que hagas al empezar a trabajar en un proyecto con service worker. Sin ella, el desarrollo es una sucesión de cambios que no aparecen, y la reacción habitual —desregistrar y recargar cada vez— es lenta y además impide probar el comportamiento real de la actualización.

La casilla vecina, la que salta el worker para todas las peticiones, es la otra que conviene conocer: hace que las peticiones vayan directamente a la red saltándose el worker por completo, sin desregistrarlo. Es la forma de comprobar si un problema viene del worker o no, en cinco segundos y sin destruir nada.

💡
Tip

Los tres ajustes que conviene tener activados mientras se desarrolla con service workers son: actualizar al recargar, desactivar la caché HTTP en el panel de red, y saltarse la red temporalmente cuando haga falta descartar al worker como sospechoso. Los tres se desactivan solos al cerrar las DevTools, así que no contaminan la experiencia real.

En producción: las dos llamadas

Saltarse la espera. Se llama desde el worker nuevo, habitualmente dentro del evento de instalación, y hace que pase directamente al estado de activación sin esperar a que se cierren los clientes del anterior.

Reclamar los clientes. Se llama desde el evento de activación y hace que el worker recién activado tome el control de las páginas que ya estaban abiertas, que de otro modo seguirían sin controlador o con el anterior hasta la siguiente navegación.

Las dos juntas producen una actualización inmediata:

// Dentro del service worker: actualizacion inmediata. Comodo y peligroso.
self.addEventListener('install', (evento) => {
  evento.waitUntil(
    caches.open('armazon-v4').then(c => c.addAll(['/', '/app.css', '/app.js']))
  );
  self.skipWaiting();          // no esperar a que se cierren los clientes
});

self.addEventListener('activate', (evento) => {
  evento.waitUntil((async () => {
    // Limpiar cachés de versiones anteriores
    const nombres = await caches.keys();
    await Promise.all(nombres
      .filter(n => n.startsWith('armazon-') && n !== 'armazon-v4')
      .map(n => caches.delete(n)));
    await self.clients.claim();   // tomar el control de las pestañas abiertas
  })());
});

Qué se rompe al saltarse la espera

Esta es la parte que casi nunca se explica y que produce los bugs más raros de esta tecnología.

Al saltarse la espera, una página que ya estaba cargada pasa a estar controlada por un worker distinto del que la sirvió. Esa página tiene en memoria el JavaScript de la versión antigua, y a partir de ese momento sus peticiones las responde la versión nueva.

Las tres formas concretas en que eso falla:

El código antiguo pide recursos que la caché nueva ya borró. Un fragmento cargado bajo demanda con un nombre de fichero versionado, que el evento de activación acaba de eliminar de la caché y que ya no existe en el servidor porque el despliegue lo sustituyó. El resultado es un error de carga de módulo en una interacción concreta, difícil de reproducir y que solo afecta a quien tenía la pestaña abierta durante el despliegue.

El código antiguo habla con una API que el worker nuevo transforma. Si el worker manipula peticiones —añade cabeceras, reescribe rutas, adapta formatos— y esa lógica cambió, el cliente antiguo recibe respuestas con una forma que no espera.

Dos pestañas quedan en versiones distintas. Una recién abierta con la nueva, otra vieja con la antigua, compartiendo el mismo almacenamiento. Si el formato de los datos guardados cambió, se corrompen mutuamente.

Por eso la regla es clara: saltarse la espera es correcto cuando el worker solo cachea recursos estáticos versionados por contenido y no transforma nada, y es arriesgado en cuanto el worker tiene lógica. Y en todos los casos, la limpieza agresiva de cachés antiguas en la activación es lo que convierte un riesgo teórico en un fallo real.

El patrón con consentimiento

La alternativa que no rompe nada y que además mejora la experiencia es no saltarse la espera automáticamente, sino detectar que hay una versión esperando y ofrecer al usuario recargar.

// En la pagina: detectar la version en espera y ofrecer la actualizacion
async function vigilarActualizaciones(registro, alHaberActualizacion) {
  // 1. Si ya hay una esperando al cargar, avisar
  if (registro.waiting) alHaberActualizacion(registro.waiting);

  // 2. Si aparece una nueva mientras la pagina esta abierta
  registro.addEventListener('updatefound', () => {
    const nuevo = registro.installing;
    nuevo?.addEventListener('statechange', () => {
      if (nuevo.state === 'installed' && navigator.serviceWorker.controller) {
        alHaberActualizacion(nuevo);
      }
    });
  });

  // 3. Comprobar periodicamente si hay version nueva en el servidor
  setInterval(() => registro.update().catch(() => {}), 60 * 60 * 1000);
}

function mostrarAvisoDeActualizacion(workerEnEspera) {
  const aviso = document.createElement('div');
  aviso.setAttribute('role', 'status');
  aviso.style.cssText =
    'position:fixed;inset:auto 16px 16px auto;padding:12px 16px;' +
    'background:#89b4fa;color:#11111b;border-radius:10px;font:14px system-ui';
  aviso.innerHTML = 'Hay una version nueva. <button id="recargar-app">Actualizar</button>';
  document.body.append(aviso);

  aviso.querySelector('#recargar-app').onclick = () => {
    // Cuando el nuevo tome el control, recargamos una sola vez
    let recargado = false;
    navigator.serviceWorker.addEventListener('controllerchange', () => {
      if (recargado) return;
      recargado = true;
      location.reload();
    });
    // Pedir al worker en espera que se active
    workerEnEspera.postMessage({ tipo: 'SALTAR_ESPERA' });
  };
}

// En el service worker, el otro extremo del mensaje:
// self.addEventListener('message', (e) => {
//   if (e.data?.tipo === 'SALTAR_ESPERA') self.skipWaiting();
// });

// Uso completo
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(reg => vigilarActualizaciones(reg, mostrarAvisoDeActualizacion))
    .catch(e => console.warn('No se pudo registrar el worker:', e));
}

Tres detalles de ese código que resuelven problemas concretos.

La bandera de recarga única. Sin ella, el evento de cambio de controlador puede producir un bucle de recargas si algo va mal. Es un bug clásico y muy molesto.

La comprobación periódica. Sin ella, un usuario que deja la pestaña abierta días nunca se entera de que hay una versión nueva, porque el navegador solo comprueba la actualización en la navegación y ocasionalmente.

El mensaje en lugar de la llamada directa. El worker que espera no se activa solo; hay que pedírselo, y el canal de mensajes es la forma de hacerlo desde la página en el momento en que el usuario da su consentimiento.

La actualización de un service worker es el único despliegue de tu producto que decide un usuario, y conviene diseñarlo como tal

Hay una inversión de control en esta tecnología que se nota poco y que cambia la naturaleza del problema: en cualquier otro sitio, tú decides cuándo tus usuarios reciben la versión nueva —despliegas y en la siguiente carga ya está—, mientras que con un service worker el momento efectivo del cambio depende del comportamiento de cada usuario: de cuándo cierre sus pestañas, de si tiene varias abiertas, de si vuelve mañana o dentro de tres semanas. El resultado es que en cualquier instante tienes una distribución de versiones en producción, no una versión, y esa distribución puede tener una cola muy larga: usuarios con una pestaña abierta en un móvil desde hace semanas, con un worker de hace tres despliegues, con una caché de hace tres despliegues, hablando con tu API actual. Las tres consecuencias que hay que asumir explícitamente en el diseño son las siguientes. Primera: tu API tiene que ser compatible hacia atrás durante todo el periodo en el que pueda quedar algún cliente antiguo vivo, y con service workers ese periodo es mucho más largo que sin ellos. Si tu proceso normal era romper compatibilidad tras una semana porque todos los clientes se recargan a diario, esa suposición deja de ser válida. Segunda: necesitas telemetría de versión. Cada petición del cliente debería identificar la versión que la envía, porque sin ese dato no tienes forma de saber qué distribución tienes ni cuándo puedes retirar una compatibilidad. Es una cabecera o un parámetro, y cuesta cinco minutos ponerlo. Tercera: necesitas un interruptor de emergencia. Si un despliegue de worker sale defectuoso, el mecanismo normal de despliegue no arregla nada, porque el worker roto es precisamente el que decide qué se sirve; hace falta que el worker actual sepa comprobar una señal remota y desregistrarse solo, o que la página tenga una ruta que fuerce la baja de todos los registros y el vaciado de todas las cachés. Sin ese interruptor, un error en el worker es la clase de incidente que se arregla pidiendo a los usuarios por correo que borren los datos del sitio, y esa es una llamada que nadie quiere hacer.