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 souber o que ele está tentando fazer nem os benefícios que ele traz, pode parecer que ele só atrapalha. Mas, depois que se entende como ele funciona, pode-se fornecer atualizações fáceis e discretas aos usuários, misturando o melhor da web com o melhor dos padrões nativos.

Essa é uma análise detalhada, mas os tópicos no início de cada seção abordam boa parte do que você precisa saber.

A intent

O objetivo do ciclo de vida é:

  • Permitir o início do desenvolvimento off-line.
  • Permitir que um novo service worker se prepare sem prejudicar o atual.
  • Garantir que uma página de dentro do escopo seja totalmente controlada pelo mesmo service worker (ou por nenhum).
  • Garanta que só haja uma versão do seu site sendo executada por vez.

Esse último ponto é muito importante. Sem os service workers, os usuários podem carregar uma guia do site e depois abrir outra. Isso pode fazer com que haja duas versões do seu site em execução ao mesmo tempo. Às vezes, não tem problema, mas se você lida com armazenamento, pode facilmente terminar com duas abas tendo opiniões diferentes sobre como gerenciar o armazenamento compartilhado. Isso pode gerar 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 a installEvent.waitUntil() sinaliza a duração e o sucesso ou a falha da instalação.
  • Um service worker não vai receber eventos como fetch e push até que a instalação seja concluída e ele se torne "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ê precisaria atualizar a página para conferir os efeitos do service worker.
  • clients.claim() pode suspender 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, vai aparecer 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 clients. Seu service worker só pode controlar clientes que estejam no escopo. Quando um cliente é "controlado", as buscas dele passam pelo service worker em escopo. Você pode detectar se um cliente é controlado por navigator.serviceWorker.controller, que será "null" ou uma instância do service worker.

Fazer o download, analisar e executar

Seu primeiro service worker é transferido quando você chama .register(). Caso seu script falhe ao fazer o download, analisar ou acionar 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 na guia "Application":

Erro exibido na guia Service Worker do DevTools

Instalar

O primeiro evento que um service worker recebe é install. Esse evento é 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 a event.waitUntil() permite que o navegador saiba quando a instalação é concluída e se ela foi bem-sucedida.

Se a promessa for rejeitada, significa que houve um erro na instalação, e o navegador descarta o service worker. Ele nunca controlará clientes. Isso significa que 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 gerenciar eventos funcionais como push e sync, você vai 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 que o service worker é ativado, ele não vai processar a solicitação e você ainda vai ver a imagem do cachorro. O padrão é consistência. Se a página carregar sem um service worker, os sub-recursos também não vão carregar. Se você carregar a demonstração pela segunda vez (em outras palavras, se atualizar a página), ela será controlada. A página e a imagem vão passar por eventos fetch, e você vai ver um gato.

clients.claim

Você pode 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, que chama clients.claim() no evento activate. Você deve ver um gato na primeira vez. Digo "deve" porque, nesse caso, há uma condição de tempo. Você só vai ver um gato se o service worker for ativado e clients.claim() entrar em vigor antes de acontecer uma tentativa de carregamento da imagem.

Se você usar o service worker para carregar páginas de forma 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 worker de serviço

Para resumir:

  • Uma atualização é acionada se ocorrer uma das seguintes situações:
    • Uma 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 do worker.
  • A maioria dos navegadores, incluindo o Chrome 68 e versões mais recentes, ignoram por padrão os cabeçalhos de cache ao verificar atualizações do script do service worker registrado. Eles ainda respeitam os cabeçalhos de cache ao buscarem recursos carregados em um service worker via importScripts(). Para modificar esse comportamento padrão, defina a opção updateViaCache ao registrar seu service worker.
  • Seu service worker é considerado atualizado se for diferente, em nível de byte, do que o navegador já tem. Estamos ampliando isso para incluir scripts/módulos importados também.
  • O service worker atualizado é inicializado junto com o que já existe e recebe o próprio evento install.
  • Se o novo worker tiver um código de status diferente de "ok" (por exemplo, 404), falhar em analisar, acionar um erro durante a execução ou for rejeitado durante a instalação, ele será descartado, mas o atual continua ativo.
  • Depois de instalado, o worker atualizado vai wait até que o worker existente 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 a uma imagem de um cavalo em vez de a 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 acima. Você ainda vai ver a imagem de um gato. Por quê?

Instalar

Mudei o nome do cache de static-v1 para static-v2. Isso significa que posso configurar o novo cache sem apagar nada no primeiro, que o service worker antigo ainda está usando.

Esse padrão cria caches específicos de cada versão, o que se parece com a forma que um aplicativo nativo agruparia recursos no próprio executável. Você também pode ter caches que não sejam específicos de 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 nenhum cliente. Esse estado é chamado de "espera" e é como o navegador garante que somente uma versão do seu service worker fique em execução por vez.

Se você executou a demonstração atualizada, ainda vai aparecer 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ê só tenha uma guia aberta para a demonstração, atualizar a página não é suficiente para permitir que a nova versão assuma. Isso acontece por causa da forma com que as navegações nos navegadores funcionam. Quando você navega, a página atual não é descartada até que os cabeçalhos de resposta sejam recebidos, e mesmo assim, se a resposta tiver um cabeçalho Content-Disposition, a página atual pode continuar lá. 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. Depois, quando você navegar de volta para a demonstração, o cavalo vai aparecer.

Esse padrão é parecido com o processo de atualização do Chrome. O download das atualizações do Chrome é feito em segundo plano, mas elas 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 formas de facilitar, o que é um dos próximos assuntos deste 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 o que não é possível fazer enquanto o worker antigo ainda está em uso, como migrar bancos de dados e apagar caches.

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

Se você passar uma promessa para event.waitUntil(), ela vai armazenar em buffer os eventos funcionais (fetch, push, sync etc.) até que a promessa seja resolvida. Então, quando o evento fetch é acionado, a ativação está completa.

Pular a fase de espera

A fase de espera indica que você está executando apenas uma versão do site de cada vez, mas não é preciso aguardar esse recurso. Você pode ativar o novo service worker antes chamando self.skipWaiting().

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

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

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

Atualizações manuais

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

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

Se acredita que o usuário usa seu site por muito tempo sem recarregar, pode ser uma boa ideia chamar update() em um intervalo (como de hora em hora).

Evite mudar o URL do script do service worker

Se você leu 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 normalmente é uma prática ruim para service workers. Só atualize o script no local atual.

Isso pode gerar um problema como este:

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

Se você fizer isso, o usuário nunca receberá sw-v2.js, porque sw-v1.js está fornecendo 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. Eca.

No entanto, para a demonstração acima, mudei o URL do service worker. Meu intuito é que você, para fins de demonstração, possa 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, temos algumas abordagens que podem ajudar:

Atualizar ao recarregar

Essa é a minha favorita.

DevTools mostrando &quot;update on reload&quot;

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

  1. Buscar 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 seu cache, atualizado.
  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 ter que recarregar ou fechar a guia.

Pular a espera

DevTools mostrando &quot;pular a espera&quot;

Se você tiver um worker em espera, clique em "skip waiting" 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. Essa abordagem não será controlada. Esse recurso está na especificação, então funciona em outros navegadores compatíveis com service workers.

Como processar 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. Sendo assim, não devemos fornecer APIs de alto nível limitadas que resolvam um problema específico usando padrões de que nós gostamos. Em vez disso, devemos dar acesso total ao navegador para permitir que você o use como quiser, da forma que funcionar melhor para seus usuários.

Por isso, para permitir o maior número possível de padrões, 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 ver, vale a pena entender o ciclo de vida do service worker. Com esse entendimento, os comportamentos do service worker vão parecer mais lógicos e menos misteriosos. Esse conhecimento vai dar mais confiança ao implantar e atualizar service workers.