Didn't make the #ChromeDevSummit this year? Catch all the content (and more!) in the Chrome Dev Summit 2019 playlist on our Chrome Developers YouTube Channel.

O ciclo de vida do Service Worker

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 ao entender como ele funciona, é possível oferecer atualizações fáceis e discretas aos usuários, combinando 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

O intent do ciclo de vida é:

  • possibilitar 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);
  • garantir que só uma versão do seu site seja 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ê lidar com armazenamento, pode facilmente terminar com duas guias tendo opiniões diferentes sobre como gerenciar o armazenamento compartilhado. Isso pode gerar erros, ou pior: perda de dados.

Atenção: os usuários odeiam perda de dados. Eles ficam extremamente desapontados.

O primeiro service worker resumido

Resumindo:

  • O evento install é o primeiro evento que um service worker recebe e 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 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 ver os efeitos do service worker.
  • clients.claim() pode modificar esse padrão e assumir o controle de páginas não controladas.

Veja este HTML:

<!DOCTYPE html>
Uma imagem aparecerá em três segundos:
<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. 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.

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.

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 se acionar um erro na execução inicial, a promessa de registro será rejeitada e o service worker será descartado.

O Chrome DevTools exibe 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. Falarei sobre 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ê 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 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 gerenciar eventos funcionais como push e sync, você terá 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 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. Caso sua página carregue sem um service worker, seus sub-recursos também carregarão dessa forma. Se você carregar a demonstração uma segunda vez (em outras palavras, se 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

Você pode controlar clientes fora do controle 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() poderá ser um problema, já que o service worker acaba controlando alguns clientes que carregaram sem ele.

Atualizar o service worker

Em resumo:

  • Uma atualização é acionada:
    • na navegação para uma página no 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.
  • A maioria dos navegadores, incluindo Chrome 68 e versões posteriores, 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(). Você pode modificar esse comportamento padrão definindo 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 continuará ativo.
  • Depois de instalado, o worker atualizado espera (wait) até que o 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 da informação acima. Você ainda verá a imagem de um gato. Veja porque…

Instalar

Veja que 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 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ê tiver executado 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. 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.

Atenção: você pode não estar atualizando a versão anterior. Talvez este seja o service worker de muitas versões atrás.

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 é acionado, a ativação está completa.

Atenção: a API de armazenamento em cache é o "armazenamento de origem" (como localStorage e IndexedDB). Se você tiver muitos sites na mesma origem (por exemplo, yourname.github.io/myapp) tome cuidado para não excluir o cache dos outros sites. Para evitar isso, dê ao cache um nome com prefixo único relacionado ao site atual, por exemplo, myapp-static-v1, e não toque neles a menos que comecem com myapp-.

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

Talvez você queira chamá-lo como resultado de um postMessage() ao service worker. Por exemplo, usar skipWaiting() após uma interação do usuário.

Veja esta 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ó verá a vaca se o novo service worker buscar, instalar e ativar antes de a página tentar carregar a imagem.

Atenção: skipWaiting() significa que seu novo service worker provavelmente controla páginas que foram carregadas com uma versão antiga. Isso quer dizer que algumas buscas da sua página serão gerenciadas pelo service worker antigo, mas o novo service worker gerenciará as buscas seguintes. Se isso puder causar problema, não use skipWaiting().

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 acreditar que o usuário usa seu site por muito tempo sem atualizar, pode ser uma boa ideia chamar update() em um intervalo (como de hora em hora).

Evitar alterar o URL do script do seu service worker

Se você tiver lido minha postagem sobre práticas recomendadas de armazenamento em cache, você pode considerar atribuir um URL único a cada versão do service worker. Não faça isso! Esta costuma ser uma prática ruim para service workers, simplesmente atualize o script na sua localização 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 em cache e disponibiliza index.html de modo que funcione em modo off-line primeiro.
  3. Você atualiza index.html de modo que registre seu novo sw-v2.js.

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

Porém, para a demonstração acima, alterei 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 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 novamente o service worker;
  2. instalá-lo 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, de modo 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ê tiver um worker em espera, pode clicar em "skip waiting" no DevTools para promovê-lo a "active" imediatamente.

Forçar recarregamento

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.

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

Você sobreviveu!

Está tudo bem! Ufa! Quanta teoria técnica. Fique atento, nas próximas semanas falaremos em detalhes sobre algumas aplicações práticas de tudo que foi abordado aqui.

Feedback

Was this page helpful?
Yes
What was the best thing about this page?
It helped me complete my goal(s)
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It had the information I needed
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It had accurate information
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It was easy to read
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
Something else
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
No
What was the worst thing about this page?
It didn't help me complete my goal(s)
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It was missing information I needed
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It had inaccurate information
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It was hard to read
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
Something else
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.