</> HTML5Advent
ENFRESDEITPT

// apis · Web Platform Advent #4

Service Workers: caching, offline e atualizações explicados

Um guia prático sobre service workers — registar um, o ciclo de vida install/activate, a Cache API, servir uma página offline e distribuir atualizações com segurança.

O ecrã de um smartphone em modo de voo a mostrar as horas, sem ligação de rede

Um service worker é um script que o navegador executa em segundo plano, separado da sua página. Coloca-se entre a sua aplicação e a rede como um proxy programável: pode intercetar pedidos, respondê-los a partir de uma cache e manter o seu site a funcionar mesmo quando o utilizador está offline. É a base das Progressive Web Apps.

Duas regras fundamentais antes de mais nada. Um service worker só funciona sobre HTTPS (ou localhost durante o desenvolvimento) e não tem acesso ao DOM — comunica com as páginas através de eventos e mensagens, não tocando em document.

Registar um service worker

Regista o worker a partir do script normal da sua página. Verifique primeiro a disponibilidade da funcionalidade, depois aponte o navegador para o ficheiro do script:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/sw.js')
      .then((reg) => console.log('Scope:', reg.scope))
      .catch((err) => console.error('SW failed:', err));
  });
}

A localização do ficheiro é importante: um worker em /sw.js controla toda a origem, enquanto um em /app/sw.js só controla páginas sob /app/. Esse limite é o seu scope.

O ciclo de vida: install e activate

Um service worker tem uma vida própria independente da página. Os dois eventos que mais lhe interessam são install (disparado uma vez, um bom sítio para pré-armazenar assets em cache) e activate (um bom sítio para limpar caches antigas):

const CACHE = 'site-v1';
const ASSETS = ['/', '/index.html', '/styles.css', '/app.js', '/offline.html'];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE).then((cache) => cache.addAll(ASSETS))
  );
});

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))
    )
  );
});

Chamar event.waitUntil() diz ao navegador para não considerar a fase concluída até que a sua promise se resolva — caso contrário, poderia encerrar o worker a meio da tarefa.

A rack of server hard-drive bays with status lights
A Cache API armazena as respostas no próprio dispositivo do utilizador, não num servidor como o ilustrado — essa cópia local é o que permite que uma página carregue sem qualquer rede.

Intercetar pedidos com fetch

O evento fetch é onde a magia acontece. Você decide o que cada pedido devolve. Uma estratégia comum e segura é cache-first com recurso à rede:

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      return cached || fetch(event.request);
    })
  );
});

Para mostrar uma página offline personalizada quando tanto a cache como a rede falham, adicione um catch:

self.addEventListener('fetch', (event) => {
  event.respondWith(
    fetch(event.request).catch(() => caches.match('/offline.html'))
  );
});

Estratégias de caching num relance

EstratégiaComportamentoBoa para
Cache-firstServir da cache, recorrer à redeAssets estáticos (CSS, JS, tipos de letra)
Network-firstTentar a rede, recorrer à cacheHTML, dados de API que mudam
Stale-while-revalidateServir da cache, atualizá-la em segundo planoAvatares, feeds, listas de conteúdo

Distribuir atualizações sem prejudicar os utilizadores

Quando altera sw.js, o navegador deteta os novos bytes e instala o novo worker, mas este permanece em espera até que cada separador que usa o antigo seja fechado. Isto impede que duas versões sejam executadas ao mesmo tempo. Para assumir o controlo mais cedo, chame self.skipWaiting() em install e clients.claim() em activate — mas só o faça quando tiver a certeza de que a nova versão é compatível com as páginas já abertas.

Incremente sempre o nome da cache (site-v1site-v2) quando os assets mudam. O handler activate acima elimina então a cache obsoleta, para que os utilizadores nunca recebam uma mistura de ficheiros antigos e novos.

Este é todo o ciclo central: registar, pré-armazenar em cache no install, limpar no activate e responder aos pedidos no fetch. Comece com uma estratégia cache-first para ficheiros estáticos e uma network-first para o seu HTML, e terá um site que carrega instantaneamente em visitas repetidas e sobrevive a uma ligação interrompida.