El ciclo de vida de un service worker y por qué es así
Los seis estados por los que pasa un service worker, la garantía que el diseño protege, cómo se detecta una versión nueva byte a byte, y por qué la espera indefinida no es un fallo sino el objetivo.
El ciclo de vida de un service worker es la parte que más frustra a quien lo usa por primera vez y la que hay que entender antes de escribir una sola línea de caché. Se despliega una versión nueva, se recarga la página y sigue apareciendo la vieja; se recarga otra vez y sigue igual. No es un fallo de la implementación: es el comportamiento especificado, y protege una garantía concreta que vale la pena conocer antes de intentar saltársela.
- Nombrar los seis estados del ciclo de vida y qué transición dispara cada uno.
- Explicar qué garantía protege la fase de espera y qué se rompe al eliminarla.
- Describir cómo el navegador decide que hay una versión nueva del script.
- Inspeccionar el estado de un registro desde código y desde las herramientas del navegador.
El diagrama completo
Un service worker es un trabajador que se sitúa entre la página y la red y puede responder a sus peticiones. Esa posición es enormemente poderosa y por eso su instalación está rodeada de cautelas: un service worker persiste en el disco del usuario, sobrevive al cierre de la pestaña y decide qué se descarga y qué no. La especificación está diseñada alrededor de una idea: nunca puede haber dos versiones distintas de tu aplicación sirviendo a la vez a la misma página.
flowchart TB
A[register desde la pagina] --> B[Descarga del script]
B --> C{Identico byte a byte al instalado}
C -->|Si| D[No pasa nada]
C -->|No| E[installing]
E --> F{La promesa de install se resuelve}
F -->|Rechaza| G[redundant]
F -->|Cumple| H{Hay ya un worker controlando clientes}
H -->|No| I[activating]
H -->|Si| J[waiting]
J --> K[Se cierran todos los clientes o se llama a skipWaiting]
K --> I
I --> L{La promesa de activate se resuelve}
L -->|Rechaza| G
L -->|Cumple| M[activated]
M --> N[Controla las navegaciones a partir de ahora]
style D fill:#94e2d5,color:#11111b
style E fill:#89b4fa,color:#11111b
style G fill:#f38ba8,color:#11111b
style J fill:#f9e2af,color:#11111b
style I fill:#89b4fa,color:#11111b
style M fill:#a6e3a1,color:#11111b
style N fill:#a6e3a1,color:#11111bLos seis estados que aparecen en registration son installing, installed, activating, activated, redundant y el caso de no existir. En la práctica hablamos de cuatro momentos: se instala, espera, se activa y controla.
Qué protege la espera
La fase de espera es la que produce toda la confusión, y también es la única razón por la que este modelo es seguro. El problema que resuelve es el siguiente.
Imagina una pestaña abierta con la versión 1 de tu aplicación. Su JavaScript pide /api/v1/pedidos y espera un formato concreto de respuesta; sus imágenes tienen los nombres con el hash de la versión 1. Ahora despliegas la versión 2, con otro endpoint y otros nombres de fichero. Si el service worker de la versión 2 tomara el control inmediatamente de esa pestaña, la página que está en pantalla —código de la versión 1— empezaría a recibir respuestas gestionadas por una caché que solo conoce la versión 2. Las peticiones a los ficheros antiguos fallarían, y el usuario vería una aplicación medio rota sin haber hecho nada.
De ahí la regla: el service worker nuevo no toma el control de ningún cliente que ya esté controlado por el anterior. Espera. Y espera hasta que no quede ninguna página controlada por la versión antigua.
Esto tiene una consecuencia que sorprende a todo el mundo la primera vez: recargar la página no basta. Una recarga normal no deja de controlar el documento en ningún instante, porque el documento nuevo se pide antes de que el viejo desaparezca; hay un solapamiento. El worker en espera sigue esperando. Para desbloquearlo hay que cerrar todas las pestañas de ese ámbito y volver a abrir, o navegar a otro sitio y volver.
El equivalente mental correcto es el de un despliegue nativo: un binario en ejecución no se sustituye a sí mismo mientras corre. Se instala al lado y toma el relevo al reiniciar. El service worker hace exactamente eso, y la “recarga” del navegador no es un reinicio en ese sentido.
Cómo se detecta una versión nueva
El navegador comprueba si hay una versión nueva del script en varios momentos: en cada navegación dentro del ámbito del worker, al llamar a registration.update() de forma explícita, y periódicamente si el worker recibe eventos de push o de sincronización. La comparación es byte a byte del fichero del propio service worker.
Tres consecuencias prácticas que causan la mayoría de los problemas de despliegue.
Si el fichero del service worker no cambia, no hay actualización. Aunque hayas desplegado un paquete de JavaScript nuevo, aunque el HTML sea otro. El único fichero que se compara es el del worker. Por eso el patrón correcto es que el script del worker contenga una lista de recursos con sus hashes, o al menos una constante de versión que cambie en cada despliegue: así el fichero cambia siempre que cambia algo.
El script del worker se pide con la caché limitada. Los navegadores acotan el uso de la caché HTTP para este fichero: un max-age mayor de veinticuatro horas se ignora al comprobar actualizaciones, precisamente para que un error de configuración no deje a los usuarios atrapados durante un año. Aun así, veinticuatro horas es mucho: la configuración correcta es servir el script del service worker con Cache-Control: no-cache, o registrar con updateViaCache: 'none'.
// El registro correcto para produccion.
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js', {
scope: '/',
// Ni el script ni sus importScripts salen de la cache HTTP
// cuando se comprueba si hay version nueva.
updateViaCache: 'none',
});
});
}
El ámbito no se puede ampliar hacia arriba. Un worker servido desde /js/sw.js solo puede controlar /js/. Para controlar todo el sitio, el fichero tiene que estar en la raíz, o el servidor tiene que enviar la cabecera Service-Worker-Allowed con el ámbito mayor. Es un detalle de seguridad: evita que alguien que solo puede subir ficheros a una carpeta secuestre el dominio entero.
Registrar después del evento load no es un detalle estético. El registro dispara la descarga del script y la instalación, que compite por ancho de banda y por hilo principal justo cuando la página está intentando pintar. En la primera visita, que es la única en la que el service worker no aporta nada, registrarlo pronto solo puede empeorar el LCP.
Ver el estado desde código
Para depurar esto no basta con mirar; hay que instrumentarlo. Este fragmento imprime cada transición y es lo primero que conviene tener a mano cuando algo no se actualiza:
async function vigilarCicloDeVida() {
const reg = await navigator.serviceWorker.getRegistration();
if (!reg) return console.log('No hay registro');
const describir = (sw, etiqueta) => {
if (!sw) return;
console.log(etiqueta, sw.state, sw.scriptURL);
sw.addEventListener('statechange', () =>
console.log(etiqueta, '->', sw.state)
);
};
describir(reg.installing, 'installing');
describir(reg.waiting, 'waiting');
describir(reg.active, 'active');
// Salta cuando empieza a instalarse una version nueva.
reg.addEventListener('updatefound', () => {
console.log('Version nueva detectada');
describir(reg.installing, 'nuevo');
});
// Salta cuando cambia quien controla esta pagina.
navigator.serviceWorker.addEventListener('controllerchange', () =>
console.log('Cambio de controlador', navigator.serviceWorker.controller?.scriptURL)
);
console.log('Controlador actual', navigator.serviceWorker.controller?.scriptURL ?? 'ninguno');
}
vigilarCicloDeVida();
Dos lecturas de ese último registro que conviene interiorizar. Si navigator.serviceWorker.controller es null teniendo un worker en estado activated, es la primera carga: el worker se instaló después de que el documento empezara a cargar, así que ese documento no está controlado y sus peticiones no pasan por él. La segunda visita sí. Y si ves un worker permanentemente en waiting, no tienes un fallo: tienes el diseño funcionando, y hay una pestaña abierta en algún sitio.
En el panel de aplicación de las herramientas del navegador está lo mismo en forma visual, más tres casillas que ahorran horas de trabajo: forzar la actualización en cada recarga, saltarse el worker para todas las peticiones, y el modo sin conexión.
Casi todo el mundo aprende esta API en el orden equivocado. Se empieza por las estrategias de caché, que son divertidas y dan resultados visibles, y se deja el ciclo de vida para cuando algo falla. Ese orden garantiza que algo falle, porque las estrategias de caché son unas cuantas líneas de fetch que se entienden en diez minutos, mientras que la disciplina de versiones es la parte difícil y la que decide si el despliegue de dentro de ocho meses va a ir bien. Dale la vuelta y todo encaja: un service worker no es una caché, es un proceso de segundo plano con su propio ciclo de despliegue, desacoplado del de tu aplicación, que se ejecuta en el disco de personas a las que no puedes llamar por teléfono. Cuando lo miras así, las incomodidades dejan de serlo y se convierten en lo que son. La espera no es un obstáculo: es lo que impide que una pestaña que lleva tres días abierta con la versión de hace tres días empiece de repente a recibir respuestas de una caché que no reconoce. La comparación byte a byte no es una rareza: es la única forma de detectar cambios que no depende de que tú te acuerdes de incrementar un número. El ámbito no ampliable no es burocracia: es la frontera de confianza. Y esa perspectiva tiene una consecuencia operativa muy concreta que separa a los equipos que sufren con esto de los que no. Los que sufren tratan el service worker como parte del paquete de la aplicación, lo despliegan con el mismo comando y se preguntan por qué el usuario ve algo viejo. Los que no sufren tratan el service worker como un componente aparte, con su propio número de versión, su propio procedimiento de despliegue y su propia forma de deshacer, y aceptan explícitamente que va a haber una ventana en la que conviven dos versiones. Diseñar para esa ventana —hacer que el servidor siga respondiendo a los ficheros antiguos durante un tiempo, que las APIs sean compatibles hacia atrás durante un despliegue, que el cambio de versión se avise al usuario en lugar de imponerse— es la mitad del trabajo. La otra mitad, la de decidir qué se cachea, es la fácil.
- Registra un service worker mínimo que solo tenga un
console.logy ejecuta el vigilante de arriba. Anota lo que ves en la primera carga y en la segunda. - Cambia un comentario del fichero del worker, despliega y recarga. Confirma que aparece
updatefoundy que el nuevo se queda enwaiting. - Con el worker en espera, recarga cinco veces. Comprueba que sigue esperando. Cierra todas las pestañas del sitio, vuelve a abrir y comprueba que ahora sí se activa.
- Mira las cabeceras con las que tu servidor entrega el fichero del service worker. Si tiene un
max-agelargo, corrígelo. - Sirve el worker desde una subcarpeta sin la cabecera
Service-Worker-Allowedy comprueba qué peticiones intercepta y cuáles no.