Il precaricamento della navigazione consente di superare il tempo di avvio del service worker effettuando le richieste in parallelo.
Riepilogo
- In alcune situazioni, il tempo di avvio del service worker può ritardare una risposta di rete.
- Disponibile nei tre principali motori del browser, il precaricamento della navigazione risolve questo problema consentendoti di effettuare la richiesta in parallelo con l'avvio del service worker.
- Puoi distinguere le richieste di precaricamento dalle normali navigazioni utilizzando un'intestazione e mostrare contenuti diversi.
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…
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:
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
.
Risposte personalizzate per i precaricamenti
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.