導覽預先載入可讓您同時提出要求,克服服務工作程式啟動時間。
摘要
- 在某些情況下,服務工作者啟動時間可能會延遲網路回應。
- 導覽預先載入功能可在三個主要瀏覽器引擎中使用,可讓您在服務工作者啟動時並行提出要求,藉此修正這個問題。
- 您可以使用標頭區分預先載入要求和一般導覽,並提供不同的內容。
問題
當您前往使用服務工作者處理擷取事件的網站時,瀏覽器會向服務工作者要求回應。這包括啟動服務工作者 (如果尚未執行),以及調度擷取事件。
開機時間取決於裝置和條件。通常約為 50 毫秒。在行動裝置上,則更接近 250 毫秒。在極端情況下 (裝置速度緩慢、CPU 處於緊急狀態),可能會超過 500 毫秒。不過,由於服務工作者會在事件之間保持醒著,時間長度由瀏覽器決定,因此您只會偶爾遇到這種延遲情形,例如使用者從新的分頁或其他網站前往您的網站時。
如果您是從快取中回應,開機時間就不會是問題,因為略過網路的優點大於開機延遲時間。不過,如果您使用網路回應,
服務工作程式啟動會導致網路要求延遲。
我們會持續縮短啟動時間,方法包括在 V8 中使用程式碼快取、略過沒有擷取事件的服務工作程、預先啟動服務工作程,以及其他最佳化方式。不過,啟動時間一律會大於零。
Facebook 向我們指出這個問題的影響,並詢問如何並行執行導覽要求:
導航預先載入功能可提供協助
導覽預先載入功能可讓您指定「當使用者提出 GET 導覽要求時,在服務工作者啟動時啟動網路要求」。
啟動延遲時間仍存在,但不會阻斷網路要求,因此使用者可以更快取得內容。
以下是實際運作影片,其中服務工作者會使用 while 迴圈,有意延遲 500 毫秒的啟動時間:
以下是示範影片。如要享有導覽預先載入的優點,您需要使用支援這項功能的瀏覽器。
啟用導覽預先載入功能
addEventListener('activate', event => {
event.waitUntil(async function() {
// Feature-detect
if (self.registration.navigationPreload) {
// Enable navigation preloads!
await self.registration.navigationPreload.enable();
}
}());
});
您隨時可以呼叫 navigationPreload.enable()
,或使用 navigationPreload.disable()
停用這項功能。不過,由於 fetch
事件需要使用此事件,因此建議您在服務工作元的 activate
事件中啟用及停用此事件。
使用預先載入的回覆
瀏覽器現在會為導覽項目執行預先載入作業,但您仍需要使用回應:
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
是會以回應解算的承諾,如果:
- 已啟用導航預先載入功能。
- 這項要求是
GET
要求。 - 這項要求是導覽要求 (瀏覽器在載入網頁 (包括 iframe) 時產生)。
否則 event.preloadResponse
仍會存在,但會以 undefined
解析。
預先載入的自訂回應
如果網頁需要網路資料,最快的方法是在服務工作者中提出要求,並建立單一串流回應,其中包含快取和網路的部分。
假設我們想顯示文章:
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;
}());
}
});
在上述程式碼中,mergeResponses
是一個小函式,用於合併每項要求的串流。也就是說,我們可以在網路內容串流傳送的同時顯示快取的標頭。
這比「應用程式殼層」模式更快,因為網路要求會與網頁要求一併提出,且內容可透過串流傳輸,無需進行重大駭客攻擊。
不過,includeURL
的要求會因 Service Worker 的啟動時間而延遲。我們也可以使用導覽預先載入功能來修正這個問題,但在這種情況下,我們不想預先載入整個網頁,而是要預先載入包含項目。
為支援此功能,系統會在每個預先載入要求中傳送標頭:
Service-Worker-Navigation-Preload: true
伺服器可利用這項功能,為導覽預先載入要求傳送與一般導覽要求不同的內容。請記得新增 Vary: Service-Worker-Navigation-Preload
標頭,讓快取知道您的回應有所不同。
我們現在可以使用預先載入要求:
// 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')
];
變更頁首
根據預設,Service-Worker-Navigation-Preload
標頭的值為 true
,但您可以將其設為任何值:
navigator.serviceWorker.ready.then(registration => {
return registration.navigationPreload.setHeaderValue(newValue);
}).then(() => {
console.log('Done!');
});
舉例來說,您可以將其設為本機快取的最新貼文 ID,這樣伺服器就只會傳回較新的資料。
取得狀態
您可以使用 getState
查詢導航預先載入的狀態:
navigator.serviceWorker.ready.then(registration => {
return registration.navigationPreload.getState();
}).then(state => {
console.log(state.enabled); // boolean
console.log(state.headerValue); // string
});
感謝 Matt Falkenhagen 和 Tsuyoshi Horo 協助開發這項功能,並協助撰寫本文。也要感謝所有參與標準化工作的人員