Acelerar o service worker com pré-carregamentos de navegação

O pré-carregamento de navegação permite que você aproveite o tempo de inicialização do service worker fazendo solicitações em paralelo.

Jake Archibald
Jake Archibald

Compatibilidade com navegadores

  • Chrome: 59.
  • Borda: 18.
  • Firefox: 99.
  • Safari: 15.4.

Origem

Resumo

O problema

Quando você navega para um site que usa um service worker para processar eventos de busca, o navegador pede uma resposta ao service worker. Isso envolve a inicialização do service worker (se ele ainda não estiver em execução) e o envio do evento busca.

O tempo de inicialização depende do dispositivo e das condições. Geralmente, é em torno de 50 ms. Em dispositivos móveis, a duração é de mais de 250 ms. Em casos extremos (dispositivos lentos, CPU em perigo), ela pode ser maior que 500 ms. No entanto, como o service worker fica ativo por um tempo determinado pelo navegador entre os eventos, você só recebe esse atraso ocasionalmente, como quando o usuário navega para seu site de uma nova guia ou de outro site.

Se você estiver respondendo a partir do cache, o tempo de inicialização não será um problema, já que o benefício de ignorar a rede é maior do que o atraso de inicialização. Mas se você estiver respondendo usando a rede...

Inicialização de SW
Solicitação de navegação

A solicitação de rede é atrasada pela inicialização do service worker.

Continuamos a reduzir o tempo de inicialização usando o armazenamento em cache de código no V8, ignorando service workers que não têm um evento de busca, iniciando service workers de forma especulativa e outras otimizações. No entanto, o tempo de inicialização sempre será maior que zero.

O Facebook nos chamou a atenção para o impacto desse problema e solicitou uma maneira de realizar solicitações de navegação em paralelo:

Inicialização de SW
Solicitação de navegação

Pré-carregamento de navegação ao resgate

O pré-carregamento de navegação é um recurso que permite dizer: "Quando o usuário fizer uma solicitação de navegação GET, inicie a solicitação de rede enquanto o service worker estiver sendo inicializado".

O atraso na inicialização ainda ocorre, mas não bloqueia a solicitação de rede, então o usuário recebe o conteúdo antes.

Aqui está um vídeo disso em ação, em que o service worker recebe um atraso de inicialização deliberado de 500 ms usando um while-loop:

Confira a demonstração. Para aproveitar os benefícios do pré-carregamento de navegação, você precisa de um navegador compatível.

Ativar pré-carregamento de navegação

addEventListener('activate', event => {
  event.waitUntil(async function() {
    // Feature-detect
    if (self.registration.navigationPreload) {
      // Enable navigation preloads!
      await self.registration.navigationPreload.enable();
    }
  }());
});

Você pode chamar navigationPreload.enable() sempre que quiser ou desativá-lo com navigationPreload.disable(). No entanto, como seu evento fetch precisa usá-lo, é melhor ativá-lo e desativá-lo no evento activate do service worker.

Como usar a resposta pré-carregada

Agora, o navegador vai executar pré-carregamentos para navegações, mas você ainda precisa usar a resposta:

addEventListener('fetch', event => {
  event.respondWith(async function() {
    // Respond from the cache if we can
    const cachedResponse = await caches.match(event.request);
    if (cachedResponse) return cachedResponse;

    // Else, use the preloaded response, if it's there
    const response = await event.preloadResponse;
    if (response) return response;

    // Else try the network.
    return fetch(event.request);
  }());
});

event.preloadResponse é uma promessa que será resolvida com uma resposta se:

  • O pré-carregamento de navegação está ativado.
  • A solicitação é GET.
  • A solicitação é uma solicitação de navegação gerada pelos navegadores ao carregar páginas, incluindo iframes.

Caso contrário, event.preloadResponse ainda estará lá, mas será resolvido com undefined.

Se sua página precisar de dados da rede, a maneira mais rápida de fazer isso é solicitá-los ao service worker e criar uma única resposta de streaming contendo partes do cache e partes da rede.

Digamos que queiramos exibir um artigo:

addEventListener('fetch', event => {
  const url = new URL(event.request.url);
  const includeURL = new URL(url);
  includeURL.pathname += 'include';

  if (isArticleURL(url)) {
    event.respondWith(async function() {
      // We're going to build a single request from multiple parts.
      const parts = [
        // The top of the page.
        caches.match('/article-top.include'),
        // The primary content
        fetch(includeURL)
          // A fallback if the network fails.
          .catch(() => caches.match('/article-offline.include')),
        // The bottom of the page
        caches.match('/article-bottom.include')
      ];

      // Merge them all together.
      const {done, response} = await mergeResponses(parts);

      // Wait until the stream is complete.
      event.waitUntil(done);

      // Return the merged response.
      return response;
    }());
  }
});

Acima, mergeResponses é uma pequena função que mescla os streams de cada solicitação. Isso significa que podemos exibir o cabeçalho armazenado em cache enquanto o conteúdo da rede é transmitido.

É mais rápido que o "shell do app" modelo, já que a solicitação de rede é feita junto com a solicitação da página, e o conteúdo pode ser transmitido sem grandes invasões.

No entanto, a solicitação de includeURL será atrasada pelo tempo de inicialização do service worker. Podemos usar o pré-carregamento de navegação para corrigir isso também, mas nesse caso não queremos pré-carregar a página inteira, queremos pré-carregar uma inclusão.

Para isso, um cabeçalho é enviado com cada solicitação de pré-carregamento:

Service-Worker-Navigation-Preload: true

O servidor pode usar isso para enviar conteúdos diferentes para solicitações de pré-carregamento de navegação do que faria para uma solicitação de navegação normal. Lembre-se de adicionar um cabeçalho Vary: Service-Worker-Navigation-Preload para que os caches saibam que suas respostas são diferentes.

Agora, podemos usar a solicitação de pré-carregamento:

// Try to use the preload
const networkContent = Promise.resolve(event.preloadResponse)
  // Else do a normal fetch
  .then(r => r || fetch(includeURL))
  // A fallback if the network fails.
  .catch(() => caches.match('/article-offline.include'));

const parts = [
  caches.match('/article-top.include'),
  networkContent,
  caches.match('/article-bottom')
];

Alterar o cabeçalho

Por padrão, o valor do cabeçalho Service-Worker-Navigation-Preload é true, mas é possível defini-lo como você quiser:

navigator.serviceWorker.ready.then(registration => {
  return registration.navigationPreload.setHeaderValue(newValue);
}).then(() => {
  console.log('Done!');
});

Você pode, por exemplo, defini-lo como o ID da última postagem armazenada localmente em cache, para que o servidor retorne apenas dados mais recentes.

Como descobrir o estado

É possível pesquisar o estado do pré-carregamento de navegação usando getState:

navigator.serviceWorker.ready.then(registration => {
  return registration.navigationPreload.getState();
}).then(state => {
  console.log(state.enabled); // boolean
  console.log(state.headerValue); // string
});

Agradecemos a Matt Falkenhagen e Tsuyoshi Horo por seu trabalho neste recurso e à ajuda com este artigo. Agradecemos muito a todos os envolvidos no esforço de padronização