Experiências de navegação instantânea

Complementando as técnicas tradicionais de pré-carregamento com service workers.

Demián Renzulli
Demián Renzulli
Gilberto Cocchi
Gilberto Cocchi

A realização de uma tarefa em um site geralmente envolve várias etapas. Por exemplo, a compra de um produto em um site de e-commerce pode envolver a pesquisa de um produto, a escolha de um item na lista de resultados, a adição do item ao carrinho e a finalização da operação.

Em termos técnicos, navegar por páginas diferentes significa fazer uma solicitação de navegação. Como regra geral, não use cabeçalhos Cache-Control de longa duração para armazenar em cache a resposta HTML de uma solicitação de navegação. Normalmente, elas precisam ser atendidas pela rede com Cache-Control: no-cache para garantir que o HTML, junto com a cadeia de solicitações de rede subsequentes, esteja (razoavelmente) atualizado. Ter que ir contra a rede toda vez que o usuário navega para uma nova página significa que cada navegação pode ser lenta. Pelo menos, significa que ela não será confiável.

Para acelerar essas solicitações, se você puder antecipar a ação do usuário, solicite essas páginas e recursos com antecedência e mantenha-os no cache por um curto período até que o usuário clique nesses links. Essa técnica é chamada de pré-carregamento e é comumente implementada adicionando tags <link rel="prefetch"> às páginas, indicando o recurso a ser pré-carregado.

Neste guia, vamos explorar diferentes maneiras de usar service workers como complemento de técnicas tradicionais de pré-carregamento.

Casos de produção

O MercadoLibre é o maior site de e-commerce da América Latina. Para acelerar a navegação, eles injetam dinamicamente tags <link rel="prefetch"> em algumas partes do fluxo. Por exemplo, nas páginas de listagem, eles buscam a próxima página de resultados assim que o usuário rola até a parte de baixo da listagem:

Captura de tela das páginas de listagem 1 e 2 do MercadoLibre e uma tag de busca antecipada de links que conecta as duas.

Os arquivos pré-buscados são solicitados com a prioridade "Mais baixa" e armazenados no cache HTTP ou no cache de memória (dependendo se o recurso pode ser armazenado em cache ou não), por um período que varia de acordo com o navegador. Por exemplo, no Chrome 85, esse valor é de 5 minutos. Os recursos são mantidos por cinco minutos, após os quais as regras normais de Cache-Control para o recurso são aplicadas.

O uso do armazenamento em cache do service worker pode ajudar a estender o ciclo de vida dos recursos de pré-carregamento além da janela de cinco minutos.

Por exemplo, o portal de esportes italiano Virgilio Sport usa service workers para fazer o pré-carregamento das postagens mais populares na página inicial. Eles também usam a API Network Information para evitar a pré-busca para usuários em uma conexão 2G.

Logotipo da Virgilio Sport.

Como resultado, em mais de três semanas de observação, a Virgilio Sport observou que os tempos de carregamento para a navegação de artigos melhoraram 78%, e o número de impressões de artigos aumentou 45%.

Captura de tela da página inicial e de artigos do Virgilio Sport, com métricas de impacto após a pré-busca.

Implementar a pré-cacheação com o Workbox

Na próxima seção, vamos usar o Workbox para mostrar como implementar diferentes técnicas de armazenamento em cache no service worker que podem ser usadas como complemento de <link rel="prefetch"> ou até mesmo como substituto dele, delegando essa tarefa completamente ao service worker.

1. Pré-cache de páginas estáticas e subrecursos de página

A pré-armazenagem em cache é a capacidade do service worker de salvar arquivos no cache durante a instalação.

Nos casos a seguir, o pré-cache é usado para alcançar um objetivo semelhante ao pré-carregamento: tornar a navegação mais rápida.

Pré-cachear páginas estáticas

Para páginas geradas no momento da criação (por exemplo, about.html, contact.html) ou em sites completamente estáticos, basta adicionar os documentos do site à lista de pré-cache para que eles já estejam disponíveis no cache sempre que o usuário os acessar:

workbox.precaching.precacheAndRoute([
  {url: '/about.html', revision: 'abcd1234'},
  // ... other entries ...
]);

Pré-cachear subrecursos da página

O pré-cache de recursos estáticos que as diferentes seções do site podem usar (por exemplo, JavaScript, CSS etc.) é uma prática recomendada geral e pode dar um impulso extra nos cenários de pré-carregamento.

Para acelerar a navegação em um site de e-commerce, use as tags <link rel="prefetch"> nas páginas de listagem para pré-carregar as páginas de detalhes dos primeiros produtos. Se você já tiver pré-armazenado os subrecursos da página do produto, isso pode tornar a navegação ainda mais rápida.

Para implementar isso:

  • Adicione uma tag <link rel="prefetch"> à página:
 <link rel="prefetch" href="/phones/smartphone-5x.html" as="document">
  • Adicione os subrecursos da página à lista de pré-cache no worker de serviço:
workbox.precaching.precacheAndRoute([
  '/styles/product-page.ac29.css',
  // ... other entries ...
]);

2. Estender a vida útil dos recursos de pré-carregamento

Como mencionado anteriormente, o <link rel="prefetch"> busca e mantém recursos no cache HTTP por um período limitado. Depois disso, as regras Cache-Control de um recurso são aplicadas. A partir do Chrome 85, esse valor é de 5 minutos.

Os Service Workers permitem que você estenda a vida útil das páginas de pré-carregamento, além de oferecer o benefício adicional de disponibilizar esses recursos para uso off-line.

No exemplo anterior, é possível complementar o <link rel="prefetch"> usado para pré-carregar uma página de produto com uma estratégia de armazenamento em cache do Workbox no ambiente de execução.

Para implementar isso:

  • Adicione uma tag <link rel="prefetch"> à página:
 <link rel="prefetch" href="/phones/smartphone-5x.html" as="document">
  • Implemente uma estratégia de armazenamento em cache no momento da execução no service worker para estes tipos de solicitações:
new workbox.strategies.StaleWhileRevalidate({
  cacheName: 'document-cache',
  plugins: [
    new workbox.expiration.Plugin({
      maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
    }),
  ],
});

Neste caso, optamos por usar uma estratégia de revalidação de dados desaturos. Nessa estratégia, as páginas podem ser solicitadas do cache e da rede em paralelo. A resposta vem do cache, se disponível, ou da rede. O cache é sempre atualizado com a resposta da rede a cada solicitação bem-sucedida.

3. Delegar a pré-busca ao service worker

Na maioria dos casos, a melhor abordagem é usar <link rel="prefetch">. A tag é uma dica de recurso projetada para tornar o pré-carregamento o mais eficiente possível.

No entanto, em alguns casos, pode ser melhor delegar essa tarefa completamente ao service worker. Por exemplo, para fazer o pré-carregamento dos primeiros produtos em uma página de listagem de produtos renderizada do lado do cliente, talvez seja necessário injetar várias tags <link rel="prefetch"> dinamicamente na página com base em uma resposta da API. Isso pode consumir momentaneamente a linha de execução principal da página e dificultar a implementação.

Em casos como esse, use uma "estratégia de comunicação da página para o service worker" para delegar a tarefa de pré-carregamento completamente ao service worker. Esse tipo de comunicação pode ser alcançado usando worker.postMessage():

Um ícone de uma página que faz comunicação bidirecional com um worker de serviço.

O pacote Workbox Window simplifica esse tipo de comunicação, abstraindo muitos detalhes da chamada em questão.

A pré-busca com a janela do Workbox pode ser implementada da seguinte maneira:

  • Na página: chame o service worker transmitindo o tipo de mensagem e a lista de URLs para pré-carregar:
const wb = new Workbox('/sw.js');
wb.register();

const prefetchResponse = await wb.messageSW({type: 'PREFETCH_URLS', urls: […]});
  • No service worker: implemente um manipulador de mensagens para emitir uma solicitação fetch() para cada URL a ser pré-carregado:
addEventListener('message', (event) => {
  if (event.data.type === 'PREFETCH_URLS') {
    // Fetch URLs and store them in the cache
  }
});