Velocizza il service worker con i precaricamenti di navigazione

Il precaricamento della navigazione ti consente di superare il tempo di avvio del service worker inviando richieste in parallelo.

Jake Archibald
Jake Archibald

Supporto dei browser

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

Origine

Riepilogo

Il problema

Quando visiti un sito che utilizza un worker di servizio per gestire gli eventi di recupero, il browser chiede una risposta al worker di servizio. Ciò comporta l'avvio del service worker (se non è già in esecuzione) e l'invio dell'evento di recupero.

Il tempo di avvio dipende dal dispositivo e dalle condizioni. Di solito è di circa 50 ms. Sui dispositivi mobili, invece, si parla di 250 ms. In casi estremi (dispositivi lenti, CPU in difficoltà) può superare i 500 ms. Tuttavia, poiché il service worker rimane attivo per un periodo di tempo determinato dal browser tra un evento e l'altro, questo ritardo si verifica solo occasionalmente, ad esempio quando l'utente accede al tuo sito da una scheda nuova o da un altro sito.

Il tempo di avvio non è un problema se rispondi dalla cache, poiché il vantaggio di saltare la rete è maggiore del ritardo di avvio. Tuttavia, se rispondi utilizzando la rete…

Avvio SW
Richiesta di navigazione

La richiesta di rete è ritardata dall'avvio del service worker.

Stiamo continuando a ridurre il tempo di avvio utilizzando la memorizzazione nella cache del codice in V8, saltando i worker di servizio che non hanno un evento di recupero, avviando i worker di servizio in modo speculativo e con altre ottimizzazioni. Tuttavia, il tempo di avvio sarà sempre maggiore di zero.

Facebook ci ha segnalato l'impatto di questo problema e ci ha chiesto un modo per eseguire richieste di navigazione in parallelo:

Avvio SW
Richiesta di navigazione

Precaricamento della navigazione accorre in soccorso

Il precaricamento della navigazione è una funzionalità che ti consente di dire: "Quando l'utente effettua una richiesta di navigazione GET, avvia la richiesta di rete durante l'avvio del service worker".

Il ritardo di avvio è ancora presente, ma non blocca la richiesta di rete, quindi l'utente riceve i contenuti prima.

Ecco un video in cui il servizio viene eseguito con un ritardo di avvio deliberato di 500 ms utilizzando un ciclo while:

Ecco la demo stessa. Per usufruire dei vantaggi del precaricamento della navigazione, è necessario un browser che lo supporti.

Attivare il precaricamento della navigazione

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

Puoi chiamare navigationPreload.enable() quando vuoi o disattivarlo con navigationPreload.disable(). Tuttavia, poiché l'evento fetch deve utilizzarlo, è meglio attivarlo e disattivarlo nell'evento fetch del tuo service worker.activate

Utilizzare la risposta precaricata

Ora il browser eseguirà i precarichi per le navigazioni, ma devi comunque utilizzare la risposta:

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 è una promessa che si risolve con una risposta se:

  • Il precaricamento della navigazione è attivo.
  • La richiesta è una richiesta GET.
  • La richiesta è una richiesta di navigazione (generata dai browser quando caricano pagine, inclusi gli iframe).

In caso contrario, event.preloadResponse è ancora presente, ma viene risolto con undefined.

Se la tua pagina ha bisogno di dati della rete, il modo più rapido è richiederli nel service worker e creare una singola risposta in streaming contenente parti della cache e parti della rete.

Supponiamo di voler visualizzare un articolo:

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;
    }());
  }
});

In questo caso, mergeResponses è una piccola funzione che unisce gli stream di ogni richiesta. Ciò significa che possiamo visualizzare l'intestazione memorizzata nella cache mentre i contenuti della rete vengono trasmessi in streaming.

Questo è più veloce del modello "app shell" perché la richiesta di rete viene effettuata insieme alla richiesta di pagina e i contenuti possono essere trasmessi in streaming senza grossi problemi.

Tuttavia, la richiesta di includeURL verrà ritardata dal tempo di avvio del service worker. Possiamo utilizzare il precaricamento della navigazione anche per risolvere questo problema, ma in questo caso non vogliamo precaricare la pagina completa, ma un'inclusione.

Per supportare questa funzionalità, viene inviata un'intestazione con ogni richiesta di precaricamento:

Service-Worker-Navigation-Preload: true

Il server può utilizzare questa informazione per inviare contenuti diversi per le richieste di precaricamento della navigazione rispetto a una normale richiesta di navigazione. Ricorda solo di aggiungere un'intestazione Vary: Service-Worker-Navigation-Preload, in modo che le cache sappiano che le tue risposte sono diverse.

Ora possiamo utilizzare la richiesta di precaricamento:

// 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')
];

Modificare l'intestazione

Per impostazione predefinita, il valore dell'intestazione Service-Worker-Navigation-Preload è true, ma puoi impostarlo su qualsiasi valore:

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

Ad esempio, puoi impostarlo sull'ID dell'ultimo post memorizzato nella cache localmente, in modo che il server restituisca solo i dati più recenti.

Acquisizione dello stato

Puoi controllare lo stato del precaricamento della navigazione utilizzando getState:

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

Un grande ringraziamento a Matt Falkenhagen e Tsuyoshi Horo per il loro lavoro su questa funzionalità e per l'aiuto fornito per la stesura di questo articolo. Un grande grazie a tutti coloro che hanno contribuito allo sforzo di standardizzazione