wandres.dev
SERVICE WORKERS EN DEVTOOLS · Depurar el SW

El ciclo de vida del service worker, visible en el panel

Los seis estados por los que pasa un worker, qué transición dispara cada evento, cómo se lee su estado en el panel, y por qué el estado de espera existe.

⏱ 19 min

Un service worker es un proceso que vive fuera de tu página, que sobrevive a que la cierres, que intercepta todas sus peticiones y que se actualiza siguiendo unas reglas que nadie adivina la primera vez. Casi todos los problemas que da vienen de no entender su ciclo de vida, y ese ciclo de vida es exactamente lo que el panel de aplicación hace visible: en qué estado está, cuál está esperando, cuántos clientes tiene y qué pasa si fuerzas cada transición.

🎯 Al terminar esta lección sabrás
  • Enumerar los estados del ciclo de vida y qué transición dispara cada uno.
  • Leer el estado actual en el panel e interpretar lo que muestra.
  • Explicar por qué existe el estado de espera y qué garantiza.
  • Distinguir el ámbito de un worker y qué peticiones controla.

El ciclo

flowchart TB
a[Registro solicitado] --> b[Descarga y analisis del script]
b --> c{Es byte a byte igual al actual}
c -->|Si| d[No pasa nada mas]
c -->|No| e[Instalando]
e --> f{Evento install correcto}
f -->|No| g[Redundante]
f -->|Si| h{Hay un worker activo con clientes}
h -->|Si| i[Instalado en espera]
h -->|No| j[Activando]
i -->|skipWaiting o cierran todos los clientes| j
j --> k[Activado y controlando]
k -->|Llega uno nuevo que lo sustituye| g
style a fill:#cba6f7,color:#11111b
style e fill:#f9e2af,color:#11111b
style i fill:#f38ba8,color:#11111b
style j fill:#89b4fa,color:#11111b
style k fill:#a6e3a1,color:#11111b
style g fill:#94e2d5,color:#11111b
style d fill:#94e2d5,color:#11111b

Instalando. Se ejecuta el evento de instalación. Es donde normalmente se precargan los recursos del armazón de la aplicación. Si la promesa que se pasa a la espera del evento se rechaza, el worker pasa a redundante y no se instala. Un fallo al descargar uno solo de los recursos precargados tumba la instalación entera.

Instalado, en espera. El worker está listo y no controla nada. Espera a que el anterior deje de tener clientes.

Activando. Se ejecuta el evento de activación. Es donde se limpian las cachés de versiones anteriores.

Activado. Controla las páginas dentro de su ámbito, o las controlará en la siguiente navegación.

Redundante. Reemplazado o fallido. No hace nada.

Hay una comprobación previa a todo eso que explica un comportamiento desconcertante: si el script descargado es byte a byte idéntico al instalado, no ocurre nada. No hay instalación, no hay activación, no hay nada. Por eso un cambio en un fichero que el worker importa, sin tocar el fichero del worker, puede no desplegarse nunca.

Leerlo en el panel

La sección de service workers del panel de aplicación muestra, para cada worker registrado en el origen:

La fuente, con la URL del script y la fecha de la última actualización.

El estado, con una etiqueta que corresponde a uno de los estados anteriores y un identificador. Si hay dos, verás uno activo y otro en espera: esa es la situación que explica el noventa por ciento de las quejas de “he desplegado y no se ve”.

Si está en ejecución o detenido. Un worker se detiene cuando lleva un rato sin trabajo y se despierta cuando lo hay. Los botones para iniciarlo y detenerlo permiten provocar las dos situaciones a mano, lo cual es imprescindible para probar el arranque en frío.

Los clientes. Las pestañas que controla, con un enlace para enfocarlas.

Y varias casillas y botones que se tratan en las lecciones siguientes: actualizar al recargar, modo desconectado, saltarse la red, actualizar, darse de baja, y los disparadores de eventos.

⚠️
Cuidado

Un worker en estado de espera es la causa más frecuente de confusión con esta tecnología. Todo funciona, no hay ningún error, el código nuevo está descargado y analizado, y aun así el usuario sigue viendo la versión antigua. Recargar no lo arregla, porque una recarga normal no cierra el cliente: sustituye el documento pero la pestaña sigue siendo cliente del worker activo. Hay que cerrar todas las pestañas del origen, o forzar la transición.

Por qué existe la espera

Esa fase parece una molestia gratuita y es una garantía importante: asegura que todas las páginas abiertas de un origen están controladas por la misma versión del worker.

Sin ella, tendrías dos pestañas del mismo sitio, una servida por el worker antiguo y otra por el nuevo, con cachés distintas y con formatos de datos potencialmente incompatibles. Peor: una sola pestaña podría empezar controlada por uno y acabar controlada por otro a mitad de una operación, recibiendo una respuesta de una versión y la siguiente de otra.

Es la misma garantía que hace segura una migración de esquema en una base de datos: nadie se queda a caballo entre dos versiones. Y el precio es que la actualización no es inmediata.

Saltarse esa fase es posible y tiene su propia lección, con las consecuencias que hay que aceptar a cambio.

El ámbito

Un worker controla las páginas cuya URL empieza por su ámbito, que por defecto es el directorio donde está su script.

Eso tiene una consecuencia práctica muy concreta: un worker en una subcarpeta no puede controlar la raíz del sitio. Si el script está en una ruta profunda, su ámbito es esa ruta y todo lo que hay por encima queda fuera. Por eso el script de un service worker se sirve casi siempre desde la raíz.

Se puede declarar un ámbito distinto del predeterminado, pero solo hacia abajo, salvo que el servidor autorice lo contrario con una cabecera específica al servir el script.

// Registro con comprobacion de estado y observacion de las transiciones
async function registrarWorker(ruta = '/sw.js') {
  if (!('serviceWorker' in navigator)) return console.warn('No soportado.');

  const registro = await navigator.serviceWorker.register(ruta, { scope: '/' });
  console.log('Ambito:', registro.scope);

  const describir = () => console.table([{
    instalando: registro.installing?.state ?? '-',
    esperando: registro.waiting?.state ?? '-',
    activo: registro.active?.state ?? '-',
    controlaEstaPagina: !!navigator.serviceWorker.controller
  }]);
  describir();

  registro.addEventListener('updatefound', () => {
    const nuevo = registro.installing;
    console.log('Hay una version nueva instalandose.');
    nuevo.addEventListener('statechange', () => {
      console.log('  estado ->', nuevo.state);
      if (nuevo.state === 'installed' && navigator.serviceWorker.controller) {
        console.warn('Version nueva lista y esperando. El usuario sigue con la antigua.');
      }
      describir();
    });
  });

  navigator.serviceWorker.addEventListener('controllerchange', () => {
    console.log('El controlador ha cambiado. Ahora manda otra version.');
  });

  return registro;
}

// Inventario de todos los registros del origen
(async () => {
  if (!('serviceWorker' in navigator)) return;
  const registros = await navigator.serviceWorker.getRegistrations();
  console.table(registros.map(r => ({
    ambito: r.scope,
    instalando: r.installing?.state ?? '-',
    esperando: r.waiting?.state ?? '-',
    activo: r.active?.state ?? '-',
    script: r.active?.scriptURL?.split('/').pop() ?? '-'
  })));
  window.__registros = registros;
  console.log('Para eliminar todos: __registros.forEach(r => r.unregister())');
})();

El evento de cambio de controlador es la señal que permite reaccionar a una activación desde la página, y es la pieza que hace posible avisar al usuario de que hay una versión nueva en lugar de dejarlo con la antigua sin saberlo.

Un service worker es la única parte de tu sitio que se despliega a sí misma, y por eso su ciclo de vida es un problema de despliegue y no de programación

La razón por la que este ciclo de vida resulta tan contraintuitivo es que se está aplicando un modelo mental equivocado. Todo lo demás en la web tiene un despliegue trivial: subes un fichero nuevo, el usuario recarga, y recibe el nuevo. La actualización es instantánea, unidireccional y no requiere ninguna coordinación, porque cada carga de página empieza de cero. Un service worker rompe esa propiedad porque es un proceso residente: existe entre cargas, sobrevive al cierre de la pestaña, y tiene estado propio en forma de cachés. Actualizarlo no es sustituir un fichero, es hacer un despliegue de un servicio que está en ejecución, con clientes conectados, con datos suyos, y sin poder pararlo. Visto así, todo el ciclo de vida deja de ser arbitrario y se convierte en lo que cualquier ingeniero reconocería: un despliegue azul y verde. El worker nuevo se instala y prepara su estado en paralelo, sin tocar al que está sirviendo; espera a que no queden clientes del antiguo; y entonces toma el relevo de golpe, sin que ningún cliente quede repartido entre los dos. La fase de espera es el corte de tráfico, y el evento de activación es el momento de limpiar lo que la versión anterior dejó. Esa relectura tiene una consecuencia práctica inmediata sobre cómo hay que trabajar: las preguntas correctas sobre un service worker son preguntas de operaciones, no de programación. Cómo se versiona el estado que persiste, qué pasa con un cliente que lleva horas abierto con la versión antigua, cómo se revierte si la versión nueva es defectuosa, cómo se avisa al usuario de que hay una actualización, y qué garantías de compatibilidad hay que mantener entre la versión del worker y la del código de la página que le habla. Ninguna de esas preguntas se responde leyendo la documentación de la API, y todas hay que responderlas antes de poner un worker en producción, porque la última de ellas —qué pasa si el worker antiguo sirve un armazón antiguo que pide una API que ya cambió— es exactamente la que produce las incidencias más difíciles de diagnosticar de toda esta tecnología.