Velocizza il service worker con i precaricamenti di navigazione

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

Giacomo Archibald
Jake Archibald

Supporto dei browser

  • 59
  • 18
  • 99
  • 15,4

Fonte

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. Solitamente sono circa 50 ms. Sui dispositivi mobili è 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 tra gli eventi per un periodo determinato dal browser, 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, in quanto il vantaggio di ignorare la rete è maggiore del ritardo di avvio. Se invece rispondi tramite la rete...

Avvio software
Richiesta di navigazione

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

Continueremo a ridurre i tempi di avvio utilizzando la memorizzazione nella cache del codice nella versione V8, saltando i service worker che non hanno un evento di recupero, lanciando 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 in parallelo le richieste di navigazione:

Avvio software
Richiesta di navigazione



E abbiamo detto "Sì, sembra giusto".

"Precarica la navigazione" ti viene in soccorso

Il precaricamento della navigazione è una funzionalità che ti consente di dire "Quando l'utente invia 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 azione, in cui al service worker viene dato un ritardo di avvio deliberato di 500 ms utilizzando un loop Mentre:

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

Attivazione del 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() in qualsiasi momento o disattivare l'app con navigationPreload.disable(). Tuttavia, poiché deve essere utilizzato dall'evento fetch, è meglio abilitarlo/disabilitarlo nell'evento activate del service worker.

Utilizzo della risposta precaricata

A questo punto il browser eseguirà 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 è attivato.
  • La richiesta è una richiesta GET.
  • La richiesta è una richiesta di navigazione (che i browser generano durante il caricamento delle pagine, inclusi gli iframe).

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

Se la tua pagina richiede dati provenienti dalla rete, il modo più rapido è richiederli al service worker e creare un'unica 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;
    }());
  }
});

Nella sezione precedente, mergeResponses è una piccola funzione che unisce i flussi di ogni richiesta. Ciò significa che possiamo visualizzare l'intestazione memorizzata nella cache durante lo streaming dei contenuti della rete.

Questa operazione è più veloce rispetto al modello "shell dell'app" perché la richiesta di rete viene effettuata insieme alla richiesta di pagina e i contenuti possono essere trasmessi in streaming senza grosse compromissioni.

Tuttavia, la richiesta per includeURL subirà un ritardo in base al tempo di avvio del service worker. Possiamo utilizzare il precaricamento di navigazione per risolvere anche questo problema, ma in questo caso non vogliamo precaricare la pagina intera, ma vogliamo 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 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 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')
];

Modifica dell'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!');
});

Puoi, ad esempio, impostarlo sull'ID dell'ultimo post memorizzato nella cache localmente, in modo che il server restituisca solo 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
});

Grazie mille a Matt Falkenhagen e Tsuyoshi Horo per il loro lavoro su questa funzionalità e per l'aiuto che hanno fornito in questo articolo. E di grande ringraziamento a tutte le persone coinvolte nell'impegno di standardizzazione

Parte della serie Newly interoperable