El ciclo de vida del service worker

El ciclo de vida del service worker es la parte más complicada de este. Si desconoces lo que intenta hacer y los beneficios que ofrece, puede parecer que molesta. Sin embargo, una vez que conoces cómo funciona, puedes ofrecer actualizaciones discretas y fluidas a los usuarios, mezclando lo mejor de los patrones web y nativos.

Este artículo es un análisis exhaustivo, pero las viñetas que figuran en el comienzo de cada sección analizan los principales conceptos que debes conocer.

El intent

El intent del ciclo de vida es el siguiente:

  • Hacer posible la perspectiva de “primero sin conexión”
  • Permitir que un nuevo service worker se prepare sin interrumpir el flujo del actual
  • Garantizar que en todo momento una página dentro del ámbito esté controlada por el mismo service worker (o por ninguno).
  • Asegúrate de que solo se ejecute una versión de tu sitio a la vez.

El último punto es muy importante. Sin service worker, los usuarios pueden cargar una pestaña en tu sitio y, más tarde, abrir otra. De esta manera, pueden ejecutarse dos versiones de tu sitio al mismo tiempo. A veces, este proceso es correcto. Sin embargo, si estás lidiando con el concepto de almacenamiento, puedes tener dos pestañas con opiniones muy diferentes acerca de cómo se debería administrar el almacenamiento compartido. Esto puede ocasionar errores o, peor aún, pérdida de datos.

El primer service worker

En resumen:

  • El evento install es el primero que obtiene un service worker y solo sucede una vez.
  • Una promesa que se pasa a installEvent.waitUntil() señala la duración y el éxito o fracaso de tu instalación.
  • Un service worker no recibirá eventos como fetch y push hasta que se termine de instalar correctamente y su estado sea "activo".
  • De forma predeterminada, los recuperaciones de una página no atravesarán un service worker, a menos que la solicitud de la página en sí lo haya hecho. Por lo tanto, tendrás que actualizar la página para ver los efectos del service worker.
  • clients.claim() puede anular esta configuración predeterminada y tomar el control de las páginas no controladas.

Toma este código HTML:

<!DOCTYPE html>
An image will appear here in 3 seconds:
<script>
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('SW registered!', reg))
    .catch(err => console.log('Boo!', err));

  setTimeout(() => {
    const img = new Image();
    img.src = '/dog.svg';
    document.body.appendChild(img);
  }, 3000);
</script>

Se registra un service worker y se agrega una imagen de un perro después de 3 segundos.

Aquí se muestra su service worker, sw.js:

self.addEventListener('install', event => {
  console.log('V1 installing…');

  // cache a cat SVG
  event.waitUntil(
    caches.open('static-v1').then(cache => cache.add('/cat.svg'))
  );
});

self.addEventListener('activate', event => {
  console.log('V1 now ready to handle fetches!');
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the cat SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/cat.svg'));
  }
});

Almacena en caché la imagen de un gato y la entrega cada vez que hay una solicitud de /dog.svg. Sin embargo, si ejecutas el ejemplo anterior, verás un perro la primera vez que cargues la página. Si actualizas la página, verás el gato.

Alcance y control

El alcance predeterminado del registro de un service worker es ./ en relación con la URL de la secuencia de comandos. Esto significa que, si registras un service worker en //example.com/foo/bar.js, el alcance predeterminado será //example.com/foo/.

Denominamos clients a las páginas, los procesos de trabajo y los procesos de trabajo compartidos. Tu service worker solo puede controlar clientes que estén dentro del alcance. Una vez que un cliente está "controlado", sus recuperaciones pasan por el service worker dentro del alcance. Puedes detectar si se controla a un cliente mediante navigator.serviceWorker.controller, que será nulo o una instancia de service worker.

Descarga, analiza y ejecuta

Cuando realizas una llamada a .register(), se descarga el primer service worker. Si tu secuencia de comandos no se descarga, no se analiza o arroja un error en su ejecución inicial, se rechaza la promesa de registro y se descarta el service worker.

DevTools de Chrome muestra el error en la consola y en la sección de service worker de la pestaña Application:

Error que aparece en la pestaña Service Workers de DevTools

Instalar

El primer evento que recibe un service worker es install. Se activa apenas se ejecuta el proceso de trabajo, y solo se lo llama una vez por service worker. Si modificas la secuencia de comandos del service worker, el navegador lo considerará un proceso de trabajo de servicio diferente, y recibirá su propio evento install. Analizaré las actualizaciones en detalle más adelante.

El evento install es tu oportunidad de almacenar en caché todo lo que necesitas para poder controlar los clientes. La promesa que pasas a event.waitUntil() permite que el navegador sepa que la instalación se completó correctamente.

Si se rechaza la promesa, significa que no se completó la instalación y el navegador elimina el service worker. Nunca controlará los clientes. Esto significa que podemos confiar en la presencia de cat.svg en la caché en nuestros eventos fetch. Es una dependencia.

Activar

Una vez que tu service worker esté listo para controlar clientes y administrar eventos funcionales como push y sync, recibirás un evento activate. Sin embargo, eso no significa que se controlará la página desde la que se llamó a .register().

La primera vez que cargas la demo, si bien se solicita dog.svg mucho después de que se activa el service worker, no se controla la solicitud y sigues viendo la imagen del perro. La configuración predeterminada es consistencia. Si tu página se carga sin un service worker, tampoco lo harán los subrecursos. Si cargas la demo otra vez (en otras palabras, si actualizas la página), se controlará la solicitud. Tanto la página como la imagen atravesarán eventos fetch, y verás un gato en lugar de un perro.

clients.claim

Puedes controlar clientes no controlados llamando a clients.claim() dentro de tu service worker una vez que este está activo.

Esta es una variación de la demostración anterior que llama a clients.claim() en su evento activate. Deberías ver un gato la primera vez. Digo “deberías”, porque es una cuestión de sincronización. Solo verás un gato si se activa el service worker y clients.claim() entra en vigencia antes de que la imagen intente cargarse.

Si usas tu service worker para cargar páginas de manera diferente con respecto a la forma en la que se hubieran cargado mediante la red, clients.claim() puede ser problemático, ya que el service worker termina controlando algunos clientes que se cargaron sin él.

Actualiza el trabajador del servicio

En resumen:

  • Se activa una actualización si ocurre alguna de las siguientes situaciones:
    • Una navegación a una página dentro del alcance
    • Un evento funcional, como push y sync, a menos que haya habido una revisión de actualización dentro de las 24 horas anteriores
    • Llamar a .register() solo si cambió la URL del service worker Sin embargo, evita cambiar la URL del trabajador.
  • La mayoría de los navegadores, incluidos Chrome 68 y versiones posteriores, ignoran de forma predeterminada los encabezados de almacenamiento en caché cuando buscan actualizaciones de la secuencia de comandos del service worker registrado. Siguen respetando los encabezados de almacenamiento en caché cuando se recuperan recursos cargados en un service worker a través de importScripts(). Puedes anular este comportamiento predeterminado si configuras la opción updateViaCache cuando registras tu service worker.
  • Tu service worker se considera actualizado si tiene una cantidad de bytes diferente con respecto al proceso que ya tiene el navegador. (Extendemos este concepto para incluir también los módulos y las secuencias de comandos importados.)
  • El service worker actualizado se inicia junto con el existente y recibe su propio evento install.
  • Si tu nuevo trabajador tiene un código de estado incorrecto (por ejemplo, 404), no se analiza, arroja un error durante la ejecución o se rechaza durante la instalación, el nuevo trabajador se elimina, pero el actual permanece activo.
  • Una vez que se instale correctamente, el trabajador actualizado wait hasta que el trabajador existente no controle ningún cliente. (Ten en cuenta que los clientes se superponen durante una actualización).
  • self.skipWaiting() evita la espera, es decir, el service worker se activa apenas finaliza su instalación.

Supongamos que modificamos la secuencia de comandos de nuestro service worker para responder con una imagen de un caballo en lugar de la de un gato:

const expectedCaches = ['static-v2'];

self.addEventListener('install', event => {
  console.log('V2 installing…');

  // cache a horse SVG into a new cache, static-v2
  event.waitUntil(
    caches.open('static-v2').then(cache => cache.add('/horse.svg'))
  );
});

self.addEventListener('activate', event => {
  // delete any caches that aren't in expectedCaches
  // which will get rid of static-v1
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.map(key => {
        if (!expectedCaches.includes(key)) {
          return caches.delete(key);
        }
      })
    )).then(() => {
      console.log('V2 now ready to handle fetches!');
    })
  );
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the horse SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/horse.svg'));
  }
});

Mira una demostración de lo anterior. Deberías seguir viendo una imagen de un gato. A continuación, te explicamos por qué…

Instalar

Ten en cuenta que cambié el nombre de la caché de static-v1 a static-v2. Esto significa que puedo configurar la nueva caché sin sobrescribir elementos de la actual, que continúa utilizando el service worker antiguo.

Mediante este patrón, se crean cachés específicos de la versión, similar a los recursos que una app nativa incluiría en el paquete junto con su archivo ejecutable. También es posible que tengas cachés que no sean específicas de la versión, como avatars.

Esperando

Luego de que el service worker actualizado se instala correctamente, no se activa hasta que el service worker actual ya no controle clientes. Este estado se denomina "esperando" y es la forma en la que el navegador garantiza que solo se ejecute una versión de tu service worker a la vez.

Si ejecutaste la demo actualizada, deberías seguir viendo una imagen de un gato, porque el trabajador V2 aún no se activó. Puedes ver el nuevo service worker en estado de espera en la pestaña "Application" de DevTools:

DevTools muestra un nuevo service worker en estado de espera

Incluso si tienes solo una pestaña abierta en la versión demo, actualizar la página no es suficiente para permitir que la nueva versión tome el control. Esto se debe a cómo funcionan las búsquedas en el navegador. Cuando navegas, la página actual no desaparece hasta que se hayan recibido los encabezados de respuesta, e incluso así la página actual puede permanecer visible si la respuesta tiene un encabezado Content-Disposition. Debido a esta superposición, el service worker actual siempre controla un cliente durante una actualización.

Para obtener la actualización, cierra o abandona todas las pestañas que utilizan el service worker actual. Luego, cuando navegues hasta la versión demo nuevamente, deberías ver el caballo.

Este patrón es similar a cómo se actualiza Chrome. Las actualizaciones de Chrome se descargan en segundo plano, pero no se aplican hasta que Chrome se reinicia. Mientras tanto, puedes seguir usando la versión actual sin interrupciones. Sin embargo, esto es un punto débil durante el desarrollo, pero DevTools tiene formas de simplificarlo, que analizaremos más adelante en este artículo.

Activar

Se activa cuando el service worker antiguo desaparece y tu nuevo service worker puede controlar clientes. Es el momento ideal para llevar a cabo tareas que no pudiste hacer mientras el proceso de trabajo antiguo todavía estaba en uso, como por ejemplo, migrar bases de datos y vaciar cachés.

En la versión demo anterior, mantengo una lista de cachés que deseo estén allí y, en el evento activate, elimino el resto, lo cual permite quitar la caché static-v1 antigua.

Si pasas una promesa a event.waitUntil(), se almacenarán en búfer los eventos funcionales (fetch, push, sync, etc.) hasta que se resuelva la promesa. Por lo tanto, cuando se activa el evento fetch, significa que la activación finalizó completamente.

Omitir la fase de espera

La fase de espera significa que solo puedes ejecutar una versión de tu sitio a la vez. Sin embargo, si no necesitas esa función, puedes hacer que tu nuevo service worker se active antes llamando a self.skipWaiting().

De esta manera, el service worker expulsa el proceso de trabajo activo actual y se activa automáticamente apenas ingresa en la fase de espera (o inmediatamente si ya se encuentra en esa fase). No hace que se omita la instalación de tu proceso de trabajo; simplemente es una fase de espera.

Realmente no importa cuándo llames a skipWaiting(), siempre y cuando sea durante la espera o antes de esta. Es bastante común llamarlo en el evento install:

self.addEventListener('install', event => {
  self.skipWaiting();

  event.waitUntil(
    // caching etc
  );
});

Sin embargo, es posible que desees realizar la llamada como consecuencia de un postMessage() al trabajador de servicio. En este caso, debes llamar a skipWaiting() después de la interacción del usuario.

Esta es una demostración que usa skipWaiting(). Deberías ver una imagen de una vaca sin tener que dejar de navegar. Como sucede con clients.claim(), se trata de una carrera, por lo que solo verás la vaca si el nuevo service worker realiza la obtención, se instala y se activa antes de que la página intente cargar la imagen.

Actualizaciones manuales

Como mencioné antes, el navegador revisa si hay actualizaciones disponibles automáticamente luego de las navegaciones y los eventos funcionales, pero también puedes activar dichas actualizaciones manualmente:

navigator.serviceWorker.register('/sw.js').then(reg => {
  // sometime later…
  reg.update();
});

Si prevés que el usuario utilizará tu sitio por un período prolongado sin volver a cargar la página, puedes establecer un intervalo de llamada a update() (por ejemplo, una hora).

Evita modificar la URL de la secuencia de comandos de tu service worker

Si leíste mi publicación sobre prácticas recomendadas de almacenamiento en caché, te recomendamos que le asignes una URL única a cada versión de tu service worker. No lo hagas. Por lo general, se trata de una práctica poco eficaz para los service worker. Simplemente, actualiza la secuencia de comandos en su ubicación actual.

Se podría generar uno de los siguientes problemas:

  1. index.html registra sw-v1.js como service worker.
  2. sw-v1.js almacena en caché y publica index.html para que funcione primero sin conexión.
  3. Actualizas index.html para que registre tu sw-v2.js nuevo y reluciente.

Si sigues los pasos anteriores, el usuario nunca recibe sw-v2.js, porque sw-v1.js proporciona la versión anterior de index.html desde su caché. Te encuentras en una posición en la que debes actualizar tu service worker. Puaj.

Sin embargo, en el caso de la demo anterior, cambie la URL del service worker. Hice esto a los efectos de la versión demo, para que puedas alternar entre las versiones. No es algo que haga en el entorno de producción.

Simplifica el desarrollo

El ciclo de vida del service worker se crea teniendo en cuenta al usuario. Sin embargo, durante el desarrollo, este concepto es un punto débil. Por suerte, existen algunas herramientas que pueden ayudarte:

Actualización cuando se vuelva a cargar

Esta es mi favorita.

Se muestra la herramienta &quot;update on reload&quot; en DevTools

Con esta herramienta, se logra que el ciclo de vida sea accesible para el programador. En cada navegación, sucede lo siguiente:

  1. Vuelve a recuperar el service worker.
  2. Se lo instala como una nueva versión (incluso si tiene la misma cantidad de bytes), lo que significa que tu evento install se ejecuta y las cachés se actualizan.
  3. Se omite la fase de espera para que se active el nuevo service worker.
  4. Navega por la página.

Esto significa que obtendrás actualizaciones en cada navegación (incluida la función de actualizar) sin necesidad de volver a cargar la página dos veces o cerrar la pestaña.

Omitir espera

Se muestra la herramienta &quot;skip waiting&quot; en DevTools

Si tienes un trabajador en espera, puedes seleccionar "skip waiting" en DevTools para activarlo inmediatamente.

Mayúsculas + Volver a cargar

Si fuerzas la recarga de la página (shift-reload), se evita el service worker por completo. No se lo podrá controlar. Esta función se encuentra en la especificación, por lo que funciona en otros navegadores que son compatibles con el service worker.

Cómo controlar actualizaciones

El service worker se diseñó como parte de la Web extensible. La idea es que nosotros, como programadores de navegadores, reconozcamos que no somos mejores que los programadores web en lo que respecta al desarrollo web. Y, como tales, no deberíamos proporcionar APIs de alto nivel estrechas que resuelvan un problema en particular mediante patrones que nos gusten *a nosotros*. En cambio, deberíamos ofrecerte acceso a la parte central del navegador y permitirte usar tu propia metodología, de forma que funcione mejor para *tus* usuarios.

Por lo tanto, para habilitar la mayor cantidad posible de patrones, debemos observar el ciclo de actualización completo:

navigator.serviceWorker.register('/sw.js').then(reg => {
  reg.installing; // the installing worker, or undefined
  reg.waiting; // the waiting worker, or undefined
  reg.active; // the active worker, or undefined

  reg.addEventListener('updatefound', () => {
    // A wild service worker has appeared in reg.installing!
    const newWorker = reg.installing;

    newWorker.state;
    // "installing" - the install event has fired, but not yet complete
    // "installed"  - install complete
    // "activating" - the activate event has fired, but not yet complete
    // "activated"  - fully active
    // "redundant"  - discarded. Either failed install, or it's been
    //                replaced by a newer version

    newWorker.addEventListener('statechange', () => {
      // newWorker.state has changed
    });
  });
});

navigator.serviceWorker.addEventListener('controllerchange', () => {
  // This fires when the service worker controlling this page
  // changes, eg a new worker has skipped waiting and become
  // the new active worker.
});

El ciclo de vida continúa

Como puedes ver, vale la pena comprender el ciclo de vida de los service workers. Con esa comprensión, los comportamientos de los service workers deberían parecer más lógicos y menos misteriosos. Ese conocimiento te dará más confianza a medida que implementes y actualices los trabajadores del servicio.