O ciclo de vida do service worker

Jake Archibald
Jake Archibald

O ciclo de vida do service worker é a parte mais complicada. Se você não sabe o que ele está tentando fazer e quais são os benefícios, pode parecer que ele está prejudicando você. Mas, depois de entender como ele funciona, é possível oferecer atualizações contínuas e discretas aos usuários, misturando o melhor da Web e os padrões nativos.

Essa é uma análise detalhada, mas os itens no início de cada seção abrangem a maior parte do que você precisa saber.

A intenção

A intenção do ciclo de vida é:

  • Possibilite a priorização do modo off-line.
  • Permitir que um novo service worker se prepare sem interromper o atual.
  • Garantir que uma página no escopo seja totalmente controlada pelo mesmo service worker (ou por nenhum).
  • Garanta que apenas uma versão do seu site seja executada por vez.

A última questão é muito importante. Sem os service workers, os usuários podem carregar uma guia para o seu site e depois abrir outra. Isso pode resultar na execução de duas versões do seu site ao mesmo tempo. Às vezes, não tem problema, mas se você estiver lidando com armazenamento, é fácil acabar com duas guias com opiniões diferentes sobre como gerenciar o armazenamento compartilhado. Isso pode resultar em erros, ou pior, perda de dados.

O primeiro service worker

Para resumir:

  • O evento install é o primeiro evento que um service worker recebe e só acontece uma vez.
  • Uma promessa transmitida para installEvent.waitUntil() sinaliza a duração e o sucesso ou falha da instalação.
  • Um service worker não receberá eventos como fetch e push até terminar a instalação e ficar "ativo".
  • Por padrão, as buscas de uma página não passam por um service worker, a menos que a solicitação da página tenha passado por um. Portanto, será necessário atualizar a página para ver os efeitos do service worker.
  • clients.claim() pode substituir esse padrão e assumir o controle de páginas não controladas.

Veja 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>

Ele registra um service worker e adiciona a imagem de um cachorro após três segundos.

Este é o 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'));
  }
});

Ele armazena uma imagem de um gato em cache e a disponibiliza sempre que há uma solicitação de /dog.svg. No entanto, se você executar o exemplo acima, verá um cachorro na primeira vez que carregar a página. Atualize e você verá o gato.

Escopo e controle

O escopo padrão do registro de um service worker é ./ em relação ao URL do script. Isso significa que, se você registrar um service worker em //example.com/foo/bar.js, ele terá um escopo padrão de //example.com/foo/.

Chamamos páginas, workers e workers compartilhados de clients. Seu service worker só pode controlar clientes que estão no escopo. Uma vez que um cliente é "controlado", suas buscas passam pelo service worker em escopo. Você pode detectar se um cliente é controlado por navigator.serviceWorker.controller, que será nulo ou por uma instância de service worker.

Fazer o download, analisar e executar

Seu primeiro service worker é transferido quando você chama .register(). Se o script não conseguir fazer o download, analisar ou gerar um erro na execução inicial, a promessa de registro será rejeitada e o service worker será descartado.

O Chrome DevTools mostra o erro no console e na seção do service worker na guia do aplicativo:

Erro exibido na guia do service worker no DevTools

Instalar

O primeiro evento que um service worker recebe é install. Ele é acionado assim que o worker é executado e só é chamado uma vez por service worker. Se você alterar o script do service worker, o navegador o considerará um service worker diferente e ele receberá o próprio evento install. Falarei das atualizações em detalhes mais tarde.

O evento install é sua chance de armazenar em cache tudo o que você precisa antes de poder controlar clientes. A promessa que você passa para event.waitUntil() informa ao navegador quando a instalação foi concluída e se foi bem-sucedida.

Se a promessa for rejeitada, significa que houve uma falha na instalação, e o navegador descarta o service worker. Ele nunca controlará clientes. Isso significa que podemos confiar na presença de cat.svg no cache em nossos eventos fetch. É uma dependência.

Ativar

Quando o service worker estiver pronto para controlar clientes e processar eventos funcionais como push e sync, você receberá um evento activate. Mas isso não significa que a página que chamou .register() será controlada.

Na primeira vez que você carregar a demonstração, mesmo que dog.svg seja solicitado muito tempo depois da ativação do service worker, ele não processará a solicitação, e você ainda verá a imagem do cachorro. O padrão é consistência. Se sua página carregar sem um service worker, os sub-recursos também serão carregados. Se você carregar a demonstração uma segunda vez (ou seja, atualizar a página), ela será controlada. A página e a imagem passarão por eventos fetch, e você verá um gato.

clients.claim

Para assumir o controle de clientes não controlados, chame clients.claim() no service worker quando ele for ativado.

Veja uma variação da demonstração acima que chama clients.claim() no evento activate. Você deve ver um gato na primeira vez. Digo "deveria", porque esta é uma questão de tempo. Você só verá um gato se o service worker for ativado e clients.claim() entrar em vigor antes de a imagem tentar ser carregada.

Se você usar o service worker para carregar páginas de maneira diferente de como elas são carregadas pela rede, clients.claim() poderá ser um problema, já que o service worker acaba controlando alguns clientes que carregaram sem ele.

Como atualizar o service worker

Para resumir:

  • Uma atualização será acionada se uma das seguintes situações acontecer:
    • Navegação para uma página no escopo.
    • Um evento funcional, como push e sync, a menos que tenha ocorrido uma verificação de atualização nas últimas 24 horas.
    • Chamar .register() somente se o URL do service worker tiver mudado. No entanto, evite alterar o URL de trabalho.
  • A maioria dos navegadores, incluindo o Chrome 68 e versões posteriores, ignora os cabeçalhos de cache ao verificar atualizações do script do service worker registrado. Eles ainda respeitam os cabeçalhos de armazenamento em cache ao buscar recursos carregados dentro de um service worker via importScripts(). Para substituir esse comportamento padrão, defina a opção updateViaCache ao registrar o service worker.
  • Seu service worker é considerado atualizado se for diferente, em nível de bytes, do que o navegador já tem. (estamos ampliando isso para incluir scripts/módulos importados também).
  • O service worker atualizado é iniciado junto com o atual e recebe o próprio evento install.
  • Se o novo worker tiver um código de status diferente de "ok" (por exemplo, 404), falhar ao analisar, gerar um erro durante a execução ou for rejeitado durante a instalação, o novo worker será descartado, mas o atual permanecerá ativo.
  • Depois de instalado, o worker atualizado vai wait até que o worker atual não esteja controlando nenhum cliente. Os clientes se sobrepõem durante uma atualização.
  • self.skipWaiting() evita a espera, o que significa que o service worker é ativado assim que a instalação é concluída.

Digamos que tenhamos alterado o script do nosso service worker para responder com uma imagem de um cavalo em vez de um 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'));
  }
});

Confira uma demonstração do que foi descrito acima. Você ainda deve ver a imagem de um gato. Aqui está por quê...

Instalar

Observe que mudei o nome do cache de static-v1 para static-v2. Isso significa que posso configurar o novo cache sem substituir o atual, que o antigo service worker ainda está usando.

Esse padrão cria caches específicos da versão, o que se parece com os recursos que um app nativo empacotaria com o executável. Você também pode ter caches que não sejam específicos da versão, como avatars.

Aguardando

Depois de instalado, o service worker atualizado atrasa a ativação até que o atual não esteja mais controlando os clientes. Esse estado é chamado de "esperando" e é como o navegador garante que somente uma versão do service worker fique em execução por vez.

Se você executou a demonstração atualizada, ainda verá a imagem de um gato, porque o worker versão 2 ainda não foi ativado. É possível ver o novo service worker aguardando na guia "Application" do DevTools:

DevTools mostrando o novo service worker esperando

Mesmo que você tenha apenas uma guia aberta para a demonstração, atualizar a página não é suficiente para permitir que a nova versão assuma. Isso se deve ao modo como a navegação no navegador funciona. Ao navegar, a página atual não é desativada até que os cabeçalhos de resposta sejam recebidos. Mesmo assim, ela pode permanecer se a resposta tiver um cabeçalho Content-Disposition. Por causa dessa sobreposição, o service worker atual está sempre controlando um cliente durante uma atualização.

Para receber a atualização, feche ou saia de todas as guias usando o service worker atual. Assim, quando você navegar até a demonstração novamente, você verá o cavalo.

Esse padrão é parecido com a atualização do Chrome. As atualizações do Chrome são baixadas em segundo plano, mas não se aplicam até que o Chrome seja reiniciado. Enquanto isso, você pode continuar usando a versão atual sem interrupções. No entanto, isso é um problema durante o desenvolvimento, mas o DevTools tem formas de facilitar isso, o que será abordado mais adiante neste artigo.

Ativar

A ativação dispara quando o service worker antigo é descartado e seu novo service worker está pronto para controlar clientes. Esse é o momento ideal para fazer o que não é possível fazer enquanto o worker antigo ainda está em uso, como migrar bancos de dados e limpar caches.

Na demonstração acima, mantenho uma lista de caches que espero que estejam lá e, no evento activate, me livro de todos os outros, o que remove o cache antigo static-v1.

Se você transmitir uma promessa para event.waitUntil(), ela vai armazenar em buffer os eventos funcionais (fetch, push, sync etc.) até que a promessa seja resolvida. Portanto, quando o evento fetch for disparado, a ativação estará totalmente concluída.

Pular a fase de espera

Na fase de espera, você está executando apenas uma versão do site por vez. No entanto, se esse recurso não é necessário, é possível ativar o novo service worker antes chamando self.skipWaiting().

Isso faz com que o service worker remova o worker ativo atual e se ative assim que entrar na fase de espera (ou imediatamente se já estiver na fase de espera). Isso não faz com que o worker pule a instalação, apenas aguarde.

Não importa quando você chama skipWaiting(), desde que seja durante ou antes da espera. É muito comum chamá-lo no evento install:

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

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

Mas é possível chamá-lo como resultado de um postMessage() para o service worker. Por exemplo, skipWaiting() após uma interação do usuário.

Veja uma demonstração que usa skipWaiting(). Você deve ver a imagem de uma vaca sem ter que sair da página. Como clients.claim(), é uma corrida, então você só verá a vaca se o novo service worker buscar, instalar e ativar antes que a página tente carregar a imagem.

Atualizações manuais

Como mencionei antes, o navegador verifica se há atualizações automaticamente após navegações e eventos funcionais, mas você também pode acioná-las manualmente:

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

Se você espera que o usuário use seu site por muito tempo sem atualizar, chame update() em um intervalo (por exemplo, de hora em hora).

Evite alterar o URL do script do service worker

Se você leu minha postagem sobre práticas recomendadas de armazenamento em cache, considere dar um URL exclusivo a cada versão do service worker. Não faça isso. Geralmente, essa não é uma prática recomendada para service workers. Basta atualizar o script no local atual.

Isso pode levar você a um problema como este:

  1. index.html registra sw-v1.js como um service worker.
  2. sw-v1.js armazena em cache e veicula index.html para que funcione primeiro off-line.
  3. Você atualiza o index.html para que ele registre seu sw-v2.js novo e brilhante.

Se você fizer isso, o usuário nunca receberá sw-v2.js, porque sw-v1.js está exibindo a versão antiga de index.html do cache. Você se colocou em uma posição em que precisa atualizar o service worker para atualizar seu service worker. Nossa.

No entanto, para a demonstração acima, alterei o URL do service worker. É assim que, para fins de demonstração, você pode alternar entre as versões. Não é algo que eu faria na produção.

Facilitar o desenvolvimento

O ciclo de vida de um service worker é criado pensando no usuário, mas durante o desenvolvimento isso é bem complicado. Felizmente há algumas ferramentas que podem ajudar:

Atualizar ao recarregar

Essa é a minha favorita.

DevTools mostrando a atualização no recarregamento

Isso muda o ciclo de vida para facilitar o desenvolvimento. Cada navegação:

  1. Busque novamente o service worker.
  2. Instale-o como uma nova versão, mesmo que tenha os mesmos bytes, o que significa que o evento install será executado e seus caches serão atualizados.
  3. Pular a fase de espera para que o novo service worker seja ativado.
  4. Navegue pela página.

Isso significa que você terá atualizações em cada navegação (incluindo atualização) sem precisar recarregar ou fechar a guia.

Pular a espera

DevTools mostrando &quot;pular a espera&quot;

Se você tem um worker em espera, pode clicar em "skip refresh" no DevTools para promovê-lo imediatamente a "active".

Shift: atualizar

Se você forçar a atualização da página, o service worker será totalmente ignorado. Isso não vai ser controlado. Esse recurso está na especificação, então funciona em outros navegadores que oferecem suporte a service workers.

Gerenciar atualizações

O service worker foi projetado como parte da Web extensível. A ideia é que nós, como desenvolvedores de navegadores, reconheçamos que não somos melhores no desenvolvimento web do que os desenvolvedores web. Por isso, não devemos fornecer APIs de alto nível restritas que resolvam um problema específico usando padrões de que gostamos, e sim dar a você acesso ao âmago do navegador e permitir que você faça isso como quiser, de uma maneira que funcione melhor para seus usuários.

Portanto, para ativar o maior número possível de padrões, todo o ciclo de atualização é observável:

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.
});

O ciclo de vida continua

Como você pode ver, vale a pena entender o ciclo de vida do service worker e, com esse entendimento, os comportamentos deles devem parecer mais lógicos e menos misteriosos. Esse conhecimento lhe dará mais confiança à medida que você implantar e atualizar service workers.