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á lutando contra você. No entanto, assim que souber como ele funciona, será possível fornecer atualizações contínuas e discretas aos usuários, misturando o melhor da Web e dos padrões nativos.

Essa é uma análise detalhada, mas os tópicos no início de cada seção abordam 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 controlada pelo mesmo service worker (ou por nenhum service worker).
  • Verifique se há apenas uma versão do seu site em execução por vez.

Essa última pergunta é muito importante. Sem os service workers, os usuários podem carregar uma guia no seu site e depois abrir outra. Isso pode fazer com que duas versões do seu site sejam executadas ao mesmo tempo. Às vezes, não há problema, mas se você lidar com armazenamento, pode facilmente acabar com duas guias tendo opiniões muito diferentes sobre como o armazenamento compartilhado deve ser gerenciado. 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 vai 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. Por isso, você precisa atualizar a página para ver os efeitos do service worker.
  • O 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 exibe sempre que há uma solicitação de /dog.svg. No entanto, se você executar o exemplo acima, vai 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 de páginas, workers e workers compartilhados de clients. Seu service worker só pode controlar clientes que estejam no escopo. Quando um cliente é "controlado", as buscas passam pelo service worker no escopo. Você pode detectar se um cliente é controlado por navigator.serviceWorker.controller, que será nulo ou uma instância de service worker.

Fazer o download, analisar e executar

Seu primeiro service worker é transferido por download quando você chama .register(). Se o script não fizer 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 de service workers da guia "Application":

Erro exibido na guia Service Worker das 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. Vou falar sobre as atualizações em detalhes mais tarde.

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

Se sua promessa for rejeitada, isso indicará que a instalação falhou e o navegador descarta o service worker. Ela nunca controla os clientes. Isso significa que não podemos depender de cat.svg estar presente no cache nos 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 depois da ativação do service worker, ele não vai processar a solicitação e você ainda verá a imagem do cachorro. O padrão é consistência. Se a página for carregada sem um service worker, seus subrecursos também não serão. Se você carregar a demonstração uma segunda vez (em outras palavras, 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

É possível assumir o controle de clientes não controlados chamando clients.claim() no service worker quando ele estiver ativado.

Confira uma variação da demonstração acima (link em inglês), que chama clients.claim() no evento activate. Você deve ver um gato na primeira vez. Eu digo "deveria", porque isso é sensível ao tempo. Você só verá um gato se o service worker for ativado e clients.claim() entrar em vigor antes da tentativa de carregamento da imagem.

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, porque ele acabará controlando alguns clientes que carregaram sem ele.

Como atualizar o service worker

Para resumir:

  • Uma atualização é acionada se uma das seguintes situações acontecer:
    • Navegação para uma página no escopo.
    • Eventos funcionais, 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 mudar o URL de trabalho.
  • A maioria dos navegadores, incluindo o Chrome 68 e versões posteriores, ignora por padrão os cabeçalhos de armazenamento em 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 em um service worker via importScripts(). É possível substituir esse comportamento padrão definindo a opção updateViaCache ao registrar o service worker.
  • Seu service worker é considerado atualizado se for diferente, em nível de byte, do que o navegador já tem. Também estamos ampliando isso para incluir scripts/módulos importados.
  • 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 na análise, gerar um erro durante a execução ou for rejeitado durante a instalação, ele será descartado, mas o atual vai permanecer ativo.
  • Depois de instalado, o worker atualizado vai wait até que o worker atual não esteja controlando nenhum cliente. Observe que 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 a 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 das informações acima. Você ainda deve ver a imagem de um gato. Eis o motivo...

Instalar

Mudei o nome do cache de static-v1 para static-v2. Isso significa que é possível configurar o novo cache sem substituir itens no atual, que o service worker antigo ainda está usando.

Esse padrão cria caches específicos da versão, como os recursos que um app nativo agruparia com o executável. Talvez você também tenha caches que não sejam específicos de uma versão, como avatars.

Aguardando

Depois de instalado, o service worker atualizado atrasa a ativação até que o service worker existente não esteja mais controlando clientes. Esse estado é chamado de "espera" e é como o navegador garante que apenas uma versão do service worker seja executada por vez.

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

DevTools mostrando o novo service worker aguardando

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 acontece devido à forma como a navegação no navegador funciona. Quando você navega, a página atual não desaparece até que os cabeçalhos de resposta sejam recebidos. Mesmo assim, a página atual pode permanecer se a resposta tiver um cabeçalho Content-Disposition. Devido a essa 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. Em seguida, quando navegar até a demonstração novamente, o cavalo vai aparecer.

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

Ativar

Isso dispara quando o service worker antigo é descartado e seu novo service worker está pronto para controlar clientes. Esse é o momento ideal para fazer coisas que não poderiam ser feitas enquanto o worker antigo ainda estava em uso, como migrar bancos de dados e limpar caches.

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

Se você transmitir uma promessa para event.waitUntil(), ela vai armazenar em buffer 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

A fase de espera significa que você está executando apenas uma versão do site de cada vez. No entanto, se esse recurso não for 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 a espera.

Não faz diferença quando você chama skipWaiting(), contanto 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 você pode querer chamá-lo como resultado de um postMessage() para o service worker. Por exemplo, você quer skipWaiting() após uma interação do usuário.

Confira uma demonstração que usa skipWaiting(). Você verá a imagem de uma vaca sem precisar sair da página. Assim como com 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ê tiver lido minha postagem sobre práticas recomendadas de armazenamento em cache, considere atribuir um URL exclusivo a cada versão do service worker. Não faça isso. Essa geralmente é uma prática não recomendada para service workers. Basta atualizar o script no local atual.

Ele pode resultar em um problema como este:

  1. index.html registra sw-v1.js como um service worker.
  2. O 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 novo sw-v2.js.

Se você fizer isso, o usuário nunca receberá sw-v2.js, porque o 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 o service worker. Nossa.

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

Como 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 ao recarregar

Isso torna o ciclo de vida fácil para o desenvolvedor. Cada navegação:

  1. Busque novamente o service worker.
  2. Instale-o como uma nova versão, mesmo que seja idêntico em bytes, o que significa que o evento install será executado e os caches serão atualizados.
  3. Pule a fase de espera para que o novo service worker seja ativado.
  4. Navegar 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 como pular a espera

Se você tiver um worker em espera, poderá clicar em "skip públicos" no DevTools para promovê-lo a "active" imediatamente.

Shift-recarregar

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

Como 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 da Web do que os desenvolvedores da Web. Por isso, não devemos fornecer APIs restritas de alto nível que resolvam um problema específico usando padrões de que nós gostamos. Em vez disso, devemos dar a você acesso ao funcionamento do navegador e permitir que você faça o que quiser, de uma forma que funcione melhor para seus usuários.

Portanto, para ativar o máximo de padrões possível, todo o ciclo de atualização pode ser observado:

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 notar, vale a pena entender o ciclo de vida do service worker e, com essa compreensão, os comportamentos deles parecem mais lógicos e menos misteriosos. Esse conhecimento dará a você mais confiança ao implantar e atualizar service workers.