運用導覽預先載入功能加快服務工作處理程序的速度

導覽預先載入可讓您同時提出要求,克服服務工作程式啟動時間。

Jake Archibald
Jake Archibald

瀏覽器支援

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

資料來源

摘要

問題

當您前往使用服務工作者處理擷取事件的網站時,瀏覽器會向服務工作者要求回應。這包括啟動服務工作者 (如果尚未執行),以及調度擷取事件。

開機時間取決於裝置和條件。通常約為 50 毫秒。在行動裝置上,則更接近 250 毫秒。在極端情況下 (裝置速度緩慢、CPU 處於緊急狀態),可能會超過 500 毫秒。不過,由於服務工作者會在事件之間保持醒著,時間長度由瀏覽器決定,因此您只會偶爾遇到這種延遲情形,例如使用者從新的分頁或其他網站前往您的網站時。

如果您是從快取中回應,開機時間就不會是問題,因為略過網路的優點大於開機延遲時間。不過,如果您使用網路回應,

SW 啟動
導覽要求

服務工作程式啟動會導致網路要求延遲。

我們會持續縮短啟動時間,方法包括在 V8 中使用程式碼快取略過沒有擷取事件的服務工作程預先啟動服務工作程,以及其他最佳化方式。不過,啟動時間一律會大於零。

Facebook 向我們指出這個問題的影響,並詢問如何並行執行導覽要求:

SW 啟動
導覽要求

導航預先載入功能可提供協助

導覽預先載入功能可讓您指定「當使用者提出 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 協助開發這項功能,並協助撰寫本文。也要感謝所有參與標準化工作的人員