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 al comienzo de cada sección abarcan la mayor parte de lo que debes saber.

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, es posible que se ejecuten al mismo tiempo dos versiones de tu sitio. 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, su permiso predeterminado es //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 un cliente se controla a través de 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 se muestra en la pestaña de Herramientas para desarrolladores del service worker

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 service worker diferente, y este 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 que cat.svg esté presente 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. El valor predeterminado es consistency. 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 del 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 vigor 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.

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 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 recuperan recursos cargados dentro de un service worker mediante 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 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 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, lo que significa que el service worker se activa apenas termina de instalarse.

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 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 en la actual, que sigue usando 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 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 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 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ó completamente.

Omitir 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 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 la imagen de una vaca sin tener que alejarte. 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 cambiar la URL de la secuencia de comandos del service worker

Si leíste mi publicación sobre prácticas recomendadas de uso del caché, podrías considerar otorgarle 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 por lo que funciona en el modo primero sin conexión.
  3. Actualizas index.html para que registre tu sw-v2.js nuevo y brillante.

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

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. 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 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 podrá controlar. Esta función se encuentra en la especificación, por lo que funciona en otros navegadores que admiten el service worker.

Cómo controlar 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. 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 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, 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 implementas y actualizas service workers.