El ciclo de vida del service worker

El ciclo de vida del service worker es la parte más complicada. Si no sabes lo que intenta hacer y los beneficios que ofrece, puede parecer que te molesta. Pero una vez que sabes cómo funciona, puedes ofrecer actualizaciones fluidas y discretas 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 al comienzo de cada sección abarcan la mayor parte de lo que debes saber.

El intent

El propósito del ciclo de vida es el siguiente:

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

El último punto es muy importante. Sin service workers, los usuarios pueden cargar una pestaña en tu sitio y, luego, abrir otra. De esta manera, es posible que se ejecuten al mismo tiempo dos versiones de tu sitio. En ocasiones, esto está bien, pero si estás lidiando con el almacenamiento, puedes terminar fácilmente en dos pestañas con opiniones muy diferentes sobre cómo se debería administrar el almacenamiento compartido. Esto puede provocar 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() indica 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 se vuelva "activo".
  • De forma predeterminada, las 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 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 la 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, su permiso predeterminado es //example.com/foo/.

Las páginas, los trabajadores y los trabajadores compartidos se llaman clients. 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 un cliente se controla a través de navigator.serviceWorker.controller, que será nulo o una instancia de service worker.

Descargar, analizar y ejecutar

Cuando llamas 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.

Las Herramientas para desarrolladores de Chrome muestran el error en la consola y en la sección de service worker de la pestaña Aplicación:

Error que se muestra en la pestaña de Herramientas para desarrolladores del service worker

Instalar

El primer evento que obtiene un service worker es install. Se activa en cuanto se ejecuta el trabajador y solo se lo llama una vez por service worker. Si modificas la secuencia de comandos del service worker, el navegador lo considerará un service worker diferente, y este recibirá su propio evento install. Analizaremos 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 cuándo se completó la instalación y si se realizó de forma correcta.

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

Activación

Una vez que tu service worker esté listo para controlar clientes y controlar 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 demostración, aunque se solicite dog.svg mucho después de que se active el service worker, no se controlará la solicitud y seguirás viendo la imagen del perro. El valor predeterminado es consistency. Si tu página se carga sin un service worker, tampoco lo harán los subrecursos. Si carga la demostración por segunda vez (es decir, actualiza la página), esta se controlará. Tanto la página como la imagen pasarán por eventos fetch, y verás un gato en su lugar.

clients.claim

Puedes tomar el control de clientes no controlados llamando a clients.claim() en tu service worker una vez que está activado.

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ía”, porque es una cuestión de sincronización. Solo verás un gato si se activa el service worker y clients.claim() entra en vigor antes de que la imagen intente cargarse.

Si usas tu service worker para cargar páginas de manera diferente a como se hubieran cargado mediante la red, clients.claim() puede causar problemas, ya que tu service worker termina controlando algunos clientes que se cargaron sin él.

Cómo actualizar el service worker

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 se haya realizado una verificación de actualizaciones en las últimas 24 horas
    • Llamar a .register() solo si cambió la URL del service worker Sin embargo, debes evitar cambiar la URL del trabajador.
  • La mayoría de los navegadores, incluidos Chrome 68 y versiones posteriores, de forma predeterminada ignoran los encabezados de almacenamiento en caché cuando se buscan actualizaciones de la secuencia de comandos registrada del service worker. Siguen respetando los encabezados de almacenamiento en caché cuando recuperan recursos cargados dentro de un service worker mediante importScripts(). Para anular este comportamiento predeterminado, configura la opción updateViaCache cuando registras tu service worker.
  • Tu service worker se considera actualizado si tiene una cantidad de bytes diferente al 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 proceso de trabajo 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 descarta, 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, lo que significa que el service worker se activa apenas finaliza su instalación.

Supongamos que cambiamos la secuencia de comandos de nuestro service worker para que responda 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'));
  }
});

Consulta una demostración de lo anterior. Deberías seguir viendo la imagen de un gato. Aquí 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 en la actual, que sigue usando el service worker antiguo.

Este patrón crea cachés específicas de la versión, similar a los recursos que una app nativa incluiría en el paquete con su 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 manera en la que el navegador garantiza que solo se ejecute una versión de tu service worker a la vez.

Si ejecutaste la demostración actualizada, deberías seguir viendo la 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 Herramientas para desarrolladores:

Herramientas para desarrolladores que muestran un nuevo service worker en espera

Incluso si solo tienes una pestaña abierta en la demostración, 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 navegaciones del navegador. Cuando navegas, la página actual no desaparece hasta que se reciben los encabezados de respuesta, e incluso entonces la página actual puede permanecer en la página 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 usan el service worker actual. Luego, cuando navegues hasta la demostración 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 Herramientas para desarrolladores tiene formas de facilitarlo, que analizaremos más adelante en este artículo.

Activación

Se activa cuando el service worker antiguo desaparece y tu nuevo service worker puede controlar clientes. Este es el momento ideal para hacer cosas que no pudiste hacer mientras el trabajador anterior aún estaba en uso, como migrar bases de datos y borrar cachés.

En la demostración anterior, mantengo una lista de cachés que espero estar allí y, en el evento activate, elimino cualquier otra, lo que quita la caché static-v1 anterior.

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ó por completo.

Omite la fase de espera

La fase de espera significa que solo estás ejecutando 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 si llamas a self.skipWaiting().

De esta manera, el service worker expulsa el trabajador activo actual y se activa a sí mismo apenas ingresa en la fase de espera (o de inmediato si ya se encuentra en esa fase). No hace que tu trabajador omita la instalación; solo espera.

No importa cuándo llamas a skipWaiting(), siempre y cuando sea durante la espera o antes de esta. Es bastante común realizar la llamada en el evento install:

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

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

Sin embargo, es posible que desees realizar la llamada como resultado de un postMessage() al service worker. En este caso, quieres skipWaiting() después de la interacción del usuario.

Esta es una demostración que usa skipWaiting(). Deberías ver la imagen de una vaca sin tener que alejarte. Al igual que clients.claim(), es una carrera, por lo que solo verás la vaca si el nuevo service worker se recupera, instala y activa antes de que la página intente cargar la imagen.

Actualizaciones manuales

Como mencioné antes, el navegador comprueba si hay actualizaciones disponibles automáticamente después de las navegaciones y los eventos funcionales, pero también puedes activarlas manualmente:

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

Si crees que el usuario utilizará tu sitio durante mucho tiempo sin volver a cargarlo, te recomendamos que establezcas un intervalo de llamada a update() (por ejemplo, una hora).

Evita cambiar la URL de la secuencia de comandos del service worker

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

Esto puede generar un problema como el siguiente:

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

Si realizas lo anterior, el usuario nunca obtiene sw-v2.js porque sw-v1.js entrega la versión anterior de index.html desde la caché. Te encuentras en una posición en la que debes actualizar tu service worker. Uy.

Sin embargo, en la demostración anterior, modifiqué la URL del service worker. Esto es así, para los fines de la demostración, puedes alternar entre las versiones. No es algo que haga en el entorno de producción.

Facilitar el desarrollo

El ciclo de vida del service worker se crea teniendo en cuenta al usuario, pero durante el desarrollo es un poco difícil. Afortunadamente, existen algunas herramientas que pueden ayudarte:

Actualizar cuando se vuelva a cargar

Esta es mi favorita.

Se muestra la opción &quot;update on reload&quot; en Herramientas para desarrolladores

Esto cambia el ciclo de vida para que sea fácil de usar para los desarrolladores. Cada navegación:

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

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

Omitir espera

Se muestra la herramienta &quot;skip wait&quot; en Herramientas para desarrolladores

Si cuentas con un proceso de trabajo en espera, puedes seleccionar "skip wait" en las Herramientas para desarrolladores para ascenderlo inmediatamente a "activo".

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 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 las actualizaciones

El service worker se diseñó como parte de la Web extensible. La idea es que nosotros, como desarrolladores de navegadores, reconozcamos que no somos mejores que los desarrolladores web en lo que respecta al desarrollo web. Por lo tanto, no deberíamos proporcionar APIs estrechas de alto nivel que resuelvan un problema particular con patrones que nos gusten, en su lugar, te brindaremos acceso a la parte central del navegador y te permitiremos hacerlo como quieras, de la manera que funcione mejor para tus usuarios.

Por lo tanto, para habilitar tantos patrones como podamos, todo el ciclo de actualización es observable:

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, es útil comprender el ciclo de vida del service worker y, 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 implementas y actualizas service workers.