The #ChromeDevSummit site is live, happening Nov 12-13 in San Francisco, CA
Check it out for details and request an invite. We'll be diving deep into modern web tech & looking ahead to the platform's future.

O ciclo de vida do Service Worker

O ciclo de vida de um service worker é a parte mais complicada. Se você não sabe o que ele está tentando fazer e quais são os benefícios que ele gera, pode parecer que ele atua contra você. 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 intenção

A intenção do ciclo de vida é:

  • Poder começar o desenvolvimento com base no 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).
  • Garantir 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 seu 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

Resumindo:

  • O evento install é o primeiro evento que um service worker recebe, e ele só acontece uma vez.
  • Uma promessa passada a installEvent.waitUntil() sinaliza a duração e o êxito ou uma falha na instalação.
  • Um service worker não receberá eventos como fetch e push até finalizar com sucesso a instalação e ficar "ativo".
  • Por padrão, as buscas de uma página não passar 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 ver 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 3 segundos.

Conheça o service worker registrado, 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 entrega-a sempre que há uma solicitação de /dog.svg. Porém, se você executar o exemplo acima, verá um cachorro na primeira vez que carregar a página. Atualize e você verá o gato.

Observação: gatos são melhores que cachorros. Porque sim.

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, seu escopo padrão será //example.com/foo/.

Chamamos de páginas, workers e clients dos workers compartilhados. Seu service worker só pode controlar clientes que estejam no escopo. Quando um cliente é "controlado", suas buscas 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.

Baixar, analisar e executar

Seu primeiro service worker é baixado quando você chama .register(). Se o seu script falhar em baixar, analisar ou acionar um erro na execução inicial, a promessa de registro é rejeitada e o service worker é descartado.

O Chrome DevTools exibe o erro no console e na seção dos service workers na guia "Application":

Erro exibido na guia Service Worker do DevTools

Instalação

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. Falarei sobre atualizações em detalhes mais para a frente.

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

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 em nossos eventos fetch. É uma dependência.

Ativação

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

Na primeira vez em que você carregar a demonstração, muito embora dog.svg seja solicitado bem depois de o service worker ser ativado, ele não gerenciará a solicitação e você ainda verá a imagem do cachorro. O padrão é consistência. Se sua página carrega sem um service worker, seus sub-recursos também carregarão dessa forma. Se você carregar a demonstração pela segunda vez (em outras palavras, atualizar a página), ela será controlada. As duas páginas e as duas imagens passarão por eventos fetch e você verá um gato dessa vez.

clients.claim

Você pode assumir o controle de clientes não controlados chamando clients.claim() dentro do service worker quando ele estiver ativo.

Veja uma variação da demonstração acima, em que clients.claim() é chamado no evento activate. Você deve ver um gato na primeira vez. Digo deve porque, nesse caso, há uma condição de tempo. Você só 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() pode ser um problema, já que o service worker acaba controlando alguns clientes que carregaram sem ele.

Observação: vejo muitas pessoas incluindo clients.claim() para todas as ocasiões, mas eu raramente faço isso. Ele só é importante no primeiro carregamento e, devido ao Progressive Enhancement, a página normalmente funciona muito bem sem um service worker.

Como atualizar o service worker

Resumindo:

  • Uma atualização é acionada:
    • Na navegação, para uma página em escopo.
    • Em eventos funcionais como push e sync, a menos que tenha ocorrido uma verificação de atualização nas últimas 24 horas.
    • Na chamada de .register() somente se o URL do service worker tiver mudado.
  • Os cabeçalhos de armazenamento em cache do script do service worker são respeitados (até 24 horas) quando se busca atualizações. Vamos tornar esse comportamento opcional, já que ele deixa as pessoas em situação difícil. Você provavelmente quer max-age de 0 no script do 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 seu 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 com sucesso, o worker atualizado espera (wait) até que o worker existente não esteja controlando nenhum cliente (observe que os clientes se sobrepõe 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'));
  }
});

Observação: não tenho uma opinião relevante sobre os cavalos.

Dê uma olhada em uma demonstração do mencionado acima. Você ainda verá a imagem de um gato. Veja por que...

Instalação

Veja que eu 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 como um aplicativo nativo agruparia ativos em seu executável. Você também pode ter caches que não sejam específicos de versão, como avatars.

Aguardando

Depois de instalado com sucesso, 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, deve continuar vendo 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 exibindo o novo service worker esperando

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 navegar de volta para a demonstração, você verá o cavalo.

Esse padrão é parecido com o processo de 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çã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.

Ativação

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 coisas que não dá para fazer enquanto o worker antigo ainda está em uso, como migrar bancos de dados e apagar caches.

Na demonstração acima, mantenho 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 event.waitUntil(), ele carregará em buffer os eventos funcionais (fetch, push, sync etc) até a promessa ser processada. Então, quando o evento fetch dispara, 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 esperar 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 entra 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.

Veja uma demonstração que usa skipWaiting(). Você deve ver a imagem de uma vaca sem ter que sair. Como com clients.claim(), é uma corrida, então você só 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 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 alterar o URL do script do seu service worker

Se você leu minha postagem sobre práticas recomendadas para armazenamento em cache, pode achar que é uma boa ideia dar a cada versão do seu service worker um URL exclusivo. Não faça isso! Essa normalmente é uma prática ruim para os service workers. Só atualize o script no seu local atual.

Veja um dos problemas que essa abordagem pode gerar:

  1. index.html registra sw-v1.js como um service worker.
  2. sw-v1.js armazena index.html em cache e o fornece para poder funcionar off-line.
  3. Você atualiza index.html para que seu novo e reluzente sw-v2.js seja registrado.

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

Porém, para a demonstração acima, alterei o URL do service worker. Meu intuito é, para fins de demonstração, poder alternar entre as versões. Não é algo que faria na produção.

Facilitando 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 no recarregamento

Essa é a minha favorita.

DevTools mostrando a atualização no recarregamento

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

  1. Buscar o service worker novamente.
  2. Instalá-lo como uma nova versão, mesmo que seja idêntico em nível de bytes, o que significa que o evento install será executado e o seu cache, atualizado.
  3. Pular 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 ter que recarregar ou fechar a guia.

Pular a espera

DevTools mostrando como pular a espera

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

Forçar recarregamento

Se você forçar o recarregamento 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 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 navegador, reconheçamos que não somos melhores no desenvolvimento web do que os desenvolvedores web. E, 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, mas sim dar a você acesso ao âmago do navegador e permitir que você o acesse como quiser, da forma que funcionar melhor para os seus usuários.

Por isso, para oferecer o máximo de padrões que podemos, vamos ver todo o ciclo de atualização:

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 as skipped waiting and become
  // the new active worker. 
});

Você sobreviveu! Está tudo bem!

Ufa! Quanta teoria técnica. Continue com a gente na próximas semanas. Vamos falar em detalhes sobre algumas aplicações práticas de tudo isso abordado aqui.