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

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

Jake archibald
Jake Archibald

Compatibilidade com navegadores

  • 59
  • 18
  • 99
  • 15,4

Origem

Resumo

O problema

Quando você acessa 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 de busca.

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

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

Inicialização de SO
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 os service workers que não têm um evento de busca, iniciando service workers especulativamente e outras otimizações. Entretanto, o tempo de inicialização sempre será maior que zero.

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

Inicialização de SO
Solicitação de navegação



E dissemos: "Sim, parece justo".

"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 está inicializando".

O atraso de inicialização continua presente, mas não bloqueia a solicitação de rede, fazendo com que o usuário receba o conteúdo mais cedo.

Veja um vídeo dele em ação, em que o service worker recebe um atraso deliberado de inicialização de 500 ms usando um "while-loop":

Esta é a demonstração em si (link em inglês). Para aproveitar os benefícios do pré-carregamento de navegação, é necessário ter um navegador com suporte.

Como ativar o pré-carregamento da navegação

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

É possível chamar navigationPreload.enable() a qualquer momento ou desativá-lo com navigationPreload.disable(). No entanto, como o evento fetch precisa usá-lo, é melhor ativá-lo ou desativá-lo no evento activate do service worker.

Como usar a resposta pré-carregada

Agora o navegador vai realizar pré-carregamentos de 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 é resolvida com uma resposta se:

  • O pré-carregamento da navegação está ativado.
  • A solicitação é GET.
  • A solicitação é uma solicitação de navegação (que os navegadores geram quando carregam páginas, incluindo iframes).

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

Se a página precisar de dados da rede, a maneira mais rápida é solicitá-los no service worker e criar uma única resposta transmitida com partes do cache e 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.

Isso é mais rápido do que o modelo "shell do app", 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 violações.

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

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

Service-Worker-Navigation-Preload: true

O servidor pode usar isso para enviar para solicitações de pré-carregamento de navegação um conteúdo diferente do que seria usado 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')
];

Alteração do cabeçalho

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

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

Você pode, por exemplo, defini-la como o código da última publicação armazenada em cache localmente para que o servidor só retorne dados mais recentes.

Como descobrir o estado

É possível procurar 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 pelo trabalho neste recurso e pela ajuda com este artigo. Agradecemos imensamente a todos os envolvidos na iniciativa de padronização

Parte da série Newly interoperável (em inglês)