Velocizza il service worker con i precaricamenti di navigazione

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

Jake Archibald
Jake Archibald

Browser Support

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

Source

Riepilogo

Il problema

Quando vai a un sito che utilizza un service worker per gestire gli eventi di recupero, il browser chiede una risposta al service worker. 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. Su dispositivo mobile è più simile a 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 gli eventi, questo ritardo si verifica solo occasionalmente, ad esempio quando l'utente accede al tuo sito da una nuova scheda 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 del software
Richiesta di navigazione

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

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

Facebook ha portato alla nostra attenzione l'impatto di questo problema e ha chiesto un modo per eseguire le richieste di navigazione in parallelo:

Avvio del software
Richiesta di navigazione

Il precaricamento della navigazione 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 mentre il service worker è in fase di avvio".

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

Ecco un video che mostra il funzionamento, in cui al service worker viene assegnato un ritardo di avvio intenzionale di 500 ms utilizzando un ciclo while:

Ecco la demo. Per usufruire dei vantaggi del precaricamento della navigazione, devi utilizzare un browser che lo supporti.

Attiva 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 abilitarlo e disabilitarlo nell'evento activate del service worker.

Utilizzare la risposta precaricata

Ora il browser eseguirà i precaricamenti 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 è attivato.
  • La richiesta è una richiesta GET.
  • La richiesta è una richiesta di navigazione (che i browser generano 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 dalla 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;
    }());
  }
});

Nell'esempio precedente, mergeResponses è una piccola funzione che unisce i flussi di ogni richiesta. Ciò significa che possiamo visualizzare l'intestazione memorizzata nella cache mentre i contenuti di rete vengono trasmessi in streaming.

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

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

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

Service-Worker-Navigation-Preload: true

Il server può utilizzare questo valore per inviare contenuti diversi per le richieste di precaricamento della navigazione rispetto a una normale richiesta di navigazione. Ricordati solo di aggiungere un'intestazione Vary: Service-Worker-Navigation-Preload, in modo che le cache sappiano che le 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 come preferisci:

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 locale, in modo che il server restituisca solo i dati più recenti.

Recupero dello stato

Puoi cercare 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 ringraziamento speciale a Matt Falkenhagen e Tsuyoshi Horo per il loro lavoro su questa funzionalità e per l'aiuto nella stesura di questo articolo. Un ringraziamento speciale a tutti coloro che hanno partecipato all'impegno di standardizzazione.