Velocizza il service worker con i precaricamenti di navigazione

Il precaricamento della navigazione consente di superare i tempi di avvio dei service worker effettuando richieste in parallelo.

Jake Archibald
Jake Archibald

Supporto dei browser

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

Origine

Riepilogo

Il problema

Quando accedi 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 la durata è di circa 50 ms. Sui dispositivi mobili, il tempo necessario è di più 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 intervallo 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 costituisce un problema se rispondi dalla cache, in quanto il vantaggio di saltare la rete è maggiore del ritardo di avvio. Ma se rispondi usando la rete...

Avvio software
Richiesta di navigazione

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

Continueremo a ridurre i tempi di avvio utilizzando la memorizzazione nella cache del codice nella V8, saltando 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 software
Richiesta di navigazione

Precarica la 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 all'avvio è ancora presente, ma non blocca la richiesta di rete, in modo che l'utente riceva i contenuti prima.

Ecco un video in azione, in cui al service worker viene assegnato un ritardo di avvio deliberato di 500 ms utilizzando un ciclo di tempo:

Questa è la demo. Per usufruire dei vantaggi del precaricamento della navigazione, devi avere un browser che lo supporti.

Attiva precaricamento di 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() in qualsiasi momento o disattivare l'opzione con navigationPreload.disable(). Tuttavia, poiché l'evento fetch deve utilizzarlo, è meglio abilitarlo e disabilitarlo nell'evento activate del tuo service worker.

Utilizzo della risposta precaricata

Ora il browser eseguirà i precaricamenti per le navigazioni, ma dovrai 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 è abilitato.
  • Si tratta di una richiesta di tipo GET.
  • La richiesta è una richiesta di navigazione (generata dai browser durante il caricamento delle pagine, iframe inclusi).

In caso contrario, event.preloadResponse è ancora presente, ma si risolve con undefined.

Se la tua pagina ha bisogno di dati dalla rete, il modo più rapido consiste nel richiederli al service worker e nel creare un'unica risposta trasmessa in streaming contenente parti dalla cache e parti dalla 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 precedenza, mergeResponses è una piccola funzione che unisce gli stream di ogni richiesta. Ciò significa che possiamo visualizzare l'intestazione memorizzata nella cache durante lo streaming dei contenuti della rete.

È più veloce della shell dell'app perché la richiesta di rete viene effettuata insieme alla richiesta di pagina e i contenuti possono essere riprodotti in streaming senza gravi compromissioni.

Tuttavia, la richiesta di includeURL subirà un ritardo rispetto al momento di avvio del service worker. Possiamo usare il precaricamento della navigazione anche per risolvere questo problema, ma in questo caso non vogliamo precaricare l'intera pagina, ma precaricare un elemento di inclusione.

A supporto di ciò, viene inviata un'intestazione con ogni richiesta di precaricamento:

Service-Worker-Navigation-Preload: true

Il server può utilizzarlo per inviare per le richieste di precaricamento della navigazione contenuti diversi rispetto a quelli che utilizza per una normale richiesta di navigazione. Ricordati 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 come preferisci:

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

Potresti, ad esempio, impostarlo sull'ID dell'ultimo post memorizzato nella cache locale, in modo che il server restituisca solo dati più recenti.

Ottenere lo 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
});

Grazie mille a Matt Falkenhagen e Tsuyoshi Horo per il loro lavoro su questa funzionalità e per aiutarci con questo articolo. Un enorme ringraziamento a tutte le persone coinvolte nel processo di standardizzazione