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

Com a pré-carga de navegação, é possível superar o tempo de inicialização do service worker fazendo solicitações em paralelo.

Jake Archibald
Jake Archibald

Browser Support

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

Source

Resumo

O problema

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

O tempo de inicialização depende do dispositivo e das condições. Normalmente, esse tempo é de cerca de 50 ms. Em dispositivos móveis, esse tempo é de 250 ms. Em casos extremos (dispositivos lentos, CPU em dificuldades), pode ser mais de 500 ms. No entanto, como o service worker permanece ativo por um tempo determinado pelo navegador entre os eventos, esse atraso só ocorre ocasionalmente, como quando o usuário navega até seu site em uma nova guia ou em outro site.

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

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

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

Continuamos reduzindo o tempo de inicialização usando o cache de código no V8, pulando service workers que não têm um evento de busca, iniciando service workers especulativamente e outras otimizações. No entanto, o tempo de inicialização será sempre maior que zero.

O Facebook chamou nossa atenção para o impacto desse problema e pediu uma maneira de fazer solicitações de navegação em paralelo:

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

Pré-carregamento de navegação para o resgate

A pré-carga 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á sendo inicializado".

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

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

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

Ativar o 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() quando quiser ou desativar com navigationPreload.disable(). No entanto, como seu evento fetch precisa usar esse recurso, é melhor ativá-lo e desativá-lo no evento activate do service worker.

Usar a resposta pré-carregada

Agora o navegador vai fazer 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 é resolvida com uma resposta se:

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

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

Se a página precisar de dados da rede, a maneira mais rápida é solicitar no service worker e criar uma única resposta transmitida que contenha partes do cache e partes da rede.

Digamos que queremos mostrar 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 fluxos de cada solicitação. Isso significa que podemos mostrar o cabeçalho armazenado em cache enquanto o conteúdo da rede é transmitido.

Isso é mais rápido do que o modelo "app shell", porque a solicitação de rede é feita junto com a solicitação de página, e o conteúdo pode ser transmitido sem hacks importantes.

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

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 conteúdo diferente para solicitações de pré-carregamento de navegação do que faria para uma solicitação de navegação regular. Não se esqueça 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') ]; 

Mudar o cabeçalho

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

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

Por exemplo, você pode definir como o ID da última postagem armazenada em cache localmente para que o servidor retorne apenas dados mais recentes.

Como acessar o estado

É possível pesquisar o estado da pré-carga 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 nesse recurso e pela ajuda com este artigo. E um grande agradecimento a todos os envolvidos no esforço de padronização.