Estratégias para armazenamento em cache do service worker

Até agora, só houve menções e pequenos snippets de código da interface Cache. Para usar service workers de maneira eficaz, é necessário adotar uma ou mais estratégias de armazenamento em cache, o que requer um pouco de familiaridade com a interface Cache.

Uma estratégia de armazenamento em cache é uma interação entre o evento fetch de um service worker e a interface Cache. A maneira como uma estratégia de armazenamento em cache é gravada depende. Por exemplo, pode ser preferível processar solicitações de recursos estáticos de maneira diferente de documentos, e isso afeta a forma como uma estratégia de armazenamento em cache é composta.

Antes de falarmos sobre as estratégias em si, vamos falar um pouco sobre o que a interface Cache não é, o que ela é e um breve resumo de alguns dos métodos que ela oferece para gerenciar caches do service worker.

A interface Cache versus o cache HTTP

Se você nunca trabalhou com a interface Cache, pode ser tentador pensar nela como igual ou pelo menos relacionada ao cache HTTP. Esse não é o caso.

  • A interface Cache é um mecanismo de armazenamento em cache totalmente separado do cache HTTP.
  • Qualquer configuração de Cache-Control usada para influenciar o cache HTTP não tem influência sobre quais recursos são armazenados na interface Cache.

É importante pensar nos caches do navegador como camadas. O cache HTTP é um cache de baixo nível orientado por pares de chave-valor com diretivas expressas em cabeçalhos HTTP.

Por outro lado, a interface Cache é um cache de alto nível acionado por uma API JavaScript. Isso oferece mais flexibilidade do que quando se usa pares de chave-valor HTTP relativamente simplificados, além de ser metade do que torna as estratégias de armazenamento em cache possíveis. Alguns métodos de API importantes relacionados aos caches do service worker são os seguintes:

  • CacheStorage.open para criar uma nova instância de Cache.
  • Cache.add e Cache.put para armazenar respostas de rede no cache de um service worker.
  • Cache.match para localizar uma resposta armazenada em cache em uma instância de Cache.
  • Cache.delete para remover uma resposta armazenada em cache de uma instância Cache.

Esses são apenas alguns exemplos. Existem outros métodos úteis, mas estes são os básicos, que serão usados mais adiante neste guia.

O modesto evento fetch

A outra metade de uma estratégia de armazenamento em cache é o evento fetch do service worker. Até agora, nesta documentação, você ouviu um pouco sobre "interceptar solicitações de rede", e é no evento fetch dentro de um service worker que isso acontece:

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('install', (event) => {
  event.waitUntil(caches.open(cacheName));
});

self.addEventListener('fetch', async (event) => {
  // Is this a request for an image?
  if (event.request.destination === 'image') {
    // Open the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Respond with the image from the cache or from the network
      return cache.match(event.request).then((cachedResponse) => {
        return cachedResponse || fetch(event.request.url).then((fetchedResponse) => {
          // Add the network response to the cache for future visits.
          // Note: we need to make a copy of the response to save it in
          // the cache and use the original as the request response.
          cache.put(event.request, fetchedResponse.clone());

          // Return the network response
          return fetchedResponse;
        });
      });
    }));
  } else {
    return;
  }
});

Este é um exemplo de brinquedo, e você pode ver em ação por conta própria, mas oferece um vislumbre do que os service workers podem fazer. O código acima faz o seguinte:

  1. Inspecione a propriedade destination da solicitação para ver se ela é uma solicitação de imagem.
  2. Se a imagem estiver no cache do service worker, disponibilize-a por ele. Caso contrário, busque a imagem na rede, armazene a resposta no cache e retorne-a.
  3. Todas as outras solicitações são transmitidas pelo service worker sem interação com o cache.

O objeto event de uma busca contém uma propriedade request com algumas informações úteis para ajudar você a identificar o tipo de cada solicitação:

  • url, que é o URL da solicitação de rede processada no momento pelo evento fetch.
  • method, que é o método da solicitação (por exemplo, GET ou POST).
  • mode, que descreve o modo da solicitação. Um valor de 'navigate' costuma ser usado para distinguir solicitações de documentos HTML de outras solicitações.
  • destination, que descreve o tipo de conteúdo que está sendo solicitado de uma maneira que evita o uso da extensão de arquivo do recurso solicitado.

Mais uma vez, a assincronia é o nome do jogo. Você se lembrará de que o evento install oferece um método event.waitUntil que aceita uma promessa e aguarda até que ela seja resolvida antes de continuar para a ativação. O evento fetch oferece um método event.respondWith semelhante que pode ser usado para retornar o resultado de uma solicitação fetch assíncrona ou uma resposta retornada pelo método match da interface Cache.

Estratégias de armazenamento em cache

Agora que você conhece um pouco as instâncias Cache e o manipulador de eventos fetch, é hora de conhecer algumas estratégias de armazenamento em cache do service worker. Embora as possibilidades sejam praticamente infinitas, este guia vai continuar com as estratégias que acompanham o Workbox, para que você tenha uma noção do que acontece nos componentes internos.

Somente cache

Mostra o fluxo da página para o service worker, para o cache.

Vamos começar com uma estratégia de armazenamento em cache simples, que chamamos de "Somente cache". É só que: quando o service worker está no controle da página, as solicitações correspondentes só vão para o cache. Isso significa que os recursos em cache precisarão ser pré-armazenados em cache para que estejam disponíveis para que o padrão funcione, e que esses recursos nunca serão atualizados no cache até que o service worker seja atualizado.

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

// Assets to precache
const precachedAssets = [
  '/possum1.jpg',
  '/possum2.jpg',
  '/possum3.jpg',
  '/possum4.jpg'
];

self.addEventListener('install', (event) => {
  // Precache assets on install
  event.waitUntil(caches.open(cacheName).then((cache) => {
    return cache.addAll(precachedAssets);
  }));
});

self.addEventListener('fetch', (event) => {
  // Is this one of our precached assets?
  const url = new URL(event.request.url);
  const isPrecachedRequest = precachedAssets.includes(url.pathname);

  if (isPrecachedRequest) {
    // Grab the precached asset from the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      return cache.match(event.request.url);
    }));
  } else {
    // Go to the network
    return;
  }
});

Acima, uma matriz de recursos é pré-armazenada em cache no momento da instalação. Quando o service worker gerencia as buscas, verificamos se o URL de solicitação processado pelo evento fetch está na matriz de recursos pré-armazenados em cache. Nesse caso, o recurso é extraído do cache e pulado a rede. Outras solicitações são transmitidas para a rede e apenas para ela. Para conferir essa estratégia em ação, confira esta demonstração com o console aberto.

Somente rede

Mostra o fluxo da página para o service worker e a rede.

O oposto de "Somente cache" é "Somente rede", em que uma solicitação é transmitida por um service worker para a rede sem nenhuma interação com o cache dele. Essa é uma boa estratégia para garantir a atualização do conteúdo (pense na marcação), mas a desvantagem é que ela nunca funcionará quando o usuário estiver off-line.

Garantir que uma solicitação seja transmitida para a rede significa apenas que você não chame event.respondWith para uma solicitação correspondente. Se quiser deixar tudo explícito, você pode colocar uma return; vazia no callback do evento fetch para as solicitações que você quer transmitir à rede. Isso é o que acontece na demonstração da estratégia "Somente cache" para solicitações que não são pré-cache.

Primeiro o armazenamento em cache, voltando para a rede

Mostra o fluxo da página para o service worker, para o cache e, em seguida, para a rede, se não estiver no cache.

É nessa estratégia que as coisas se envolvem um pouco mais. Para solicitações correspondentes, o processo é o seguinte:

  1. A solicitação atinge o cache. Se o recurso estiver no cache, veicule-o de lá.
  2. Se a solicitação não estiver no cache, acesse a rede.
  3. Quando a solicitação de rede for concluída, adicione-a ao cache e retorne a resposta da rede.

Confira um exemplo dessa estratégia que pode ser testado em uma demonstração ao vivo:

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  // Check if this is a request for an image
  if (event.request.destination === 'image') {
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Go to the cache first
      return cache.match(event.request.url).then((cachedResponse) => {
        // Return a cached response if we have one
        if (cachedResponse) {
          return cachedResponse;
        }

        // Otherwise, hit the network
        return fetch(event.request).then((fetchedResponse) => {
          // Add the network response to the cache for later visits
          cache.put(event.request, fetchedResponse.clone());

          // Return the network response
          return fetchedResponse;
        });
      });
    }));
  } else {
    return;
  }
});

Esse exemplo abrange apenas imagens, mas essa é uma ótima estratégia para aplicar a todos os recursos estáticos (como CSS, JavaScript, imagens e fontes), especialmente os com controle de versão com hash. Ele oferece um aumento de velocidade para recursos imutáveis, evitando todas as verificações de atualização de conteúdo com o servidor que o cache HTTP pode iniciar. O mais importante é que todos os recursos em cache vão ficar disponíveis off-line.

Priorização da rede, voltando ao cache

Mostra o fluxo da página para o service worker, para a rede e, em seguida, para o cache se a rede não estiver disponível.

Se você virar "Cache primeiro, segundo a rede", você vai acabar com a estratégia "Em primeiro lugar, depois em cache", que é assim:

  1. Primeiro, você acessa a rede em busca de uma solicitação e coloca a resposta no cache.
  2. Se você estiver off-line em algum momento, vai voltar para a versão mais recente dessa resposta no cache.

Essa estratégia é ótima para solicitações HTML ou de API quando, enquanto você está on-line, quer a versão mais recente de um recurso, mas não quer fornecer acesso off-line à versão mais recente disponível. Veja como o código fica quando aplicado a solicitações de HTML:

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  // Check if this is a navigation request
  if (event.request.mode === 'navigate') {
    // Open the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Go to the network first
      return fetch(event.request.url).then((fetchedResponse) => {
        cache.put(event.request, fetchedResponse.clone());

        return fetchedResponse;
      }).catch(() => {
        // If the network is unavailable, get
        return cache.match(event.request.url);
      });
    }));
  } else {
    return;
  }
});

Teste isso em uma demonstração. Primeiro, acesse a página. Talvez seja necessário recarregar para que a resposta HTML seja colocada no cache. Em seguida, nas ferramentas para desenvolvedores, simule uma conexão off-line e recarregue. A última versão disponível será disponibilizada instantaneamente do cache.

Em situações em que o recurso off-line é importante, mas você precisa equilibrar esse recurso com o acesso à versão mais recente de uma marcação ou de dados da API, a estratégia "Em primeiro lugar, depois em cache" é uma estratégia sólida para atingir esse objetivo.

Desatualizado durante a revalidação

Mostra o fluxo da página para o service worker, para o cache e, em seguida, da rede para o cache.

Das estratégias que abordamos até agora, "Desatualizada durante a revalidação" é a mais complexa. De certa forma, ela é semelhante às duas últimas estratégias, mas o procedimento prioriza a velocidade de acesso a um recurso e o mantém atualizado em segundo plano. Essa estratégia é semelhante a esta:

  1. Na primeira solicitação de um recurso, busque-o na rede, coloque-o no cache e retorne a resposta da rede.
  2. Nas solicitações subsequentes, exiba o recurso a partir do cache primeiro e, em seguida, "em segundo plano", solicite-o novamente pela rede e atualize a entrada do cache do recurso.
  3. Para solicitações posteriores, você vai receber a última versão buscada na rede e que foi colocada no cache na etapa anterior.

Essa é uma excelente estratégia para itens que são importantes para se manter atualizados, mas não são essenciais. Pense em itens como avatares para um site de mídia social. Eles são atualizados quando os usuários começam a fazer isso, mas a versão mais recente não é estritamente necessária em todas as solicitações.

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  if (event.request.destination === 'image') {
    event.respondWith(caches.open(cacheName).then((cache) => {
      return cache.match(event.request).then((cachedResponse) => {
        const fetchedResponse = fetch(event.request).then((networkResponse) => {
          cache.put(event.request, networkResponse.clone());

          return networkResponse;
        });

        return cachedResponse || fetchedResponse;
      });
    }));
  } else {
    return;
  }
});

Confira como isso funciona em outra demonstração ao vivo, especialmente se você prestar atenção na guia "Rede" nas ferramentas para desenvolvedores do seu navegador e no visualizador de CacheStorage, se as ferramentas para desenvolvedores do seu navegador tiverem essa ferramenta.

Vamos para o Workbox.

Neste documento, encerramos nossa revisão da API do service worker e das APIs relacionadas. Isso significa que você aprendeu o suficiente sobre como usar os service workers diretamente para começar a mexer no Workbox.