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

導覽預先載入功能可讓您同時發出要求,藉此克服服務工作站的啟動時間。

阿奇巴德 (Jake Archibald)
Jake Archibald

瀏覽器支援

  • 59
  • 18
  • 99
  • 15.4

資料來源

摘要

問題所在

當您前往使用 Service Worker 處理擷取事件的網站時,瀏覽器會要求服務工作處理程序回應。這包括啟動 Service Worker (如果尚未啟動),以及分派擷取事件。

開機時間視裝置和條件而定,通常會約 50 毫秒。就像 250 毫秒一樣。在極少數的情況下 (裝置速度慢,CPU 造成痛苦),可能會超過 500 毫秒。不過,由於服務工作處理程序會在瀏覽器指定的事件間隔時間保持喚醒狀態,因此偶爾只會發生這類延遲,例如使用者從新分頁或其他網站前往您的網站時。

如果您從快取回應,開機時間並不會造成問題,因為略過網路的好處會大於開機延遲。但如果您是用網路回應...

SW 靴子
導航要求

Service Worker 啟動時的網路要求會延遲。

為持續縮短啟動時間,我們使用 V8 中的程式碼快取功能,透過推測性啟動 Service Worker 及其他最佳化方式,略過沒有擷取事件的 Service Worker。不過,開機時間一律大於零。

Facebook 注意到這個問題的影響,並詢問能否同時執行導航要求:

SW 靴子
導航要求



假如我們說「沒錯,還算公平」。

「預先載入導覽」至救援

導覽預先載入功能可讓您說出「Ok,當使用者提出 GET 導航要求時,在 Service Worker 啟動時啟動網路要求」。

系統依然會顯示啟動延遲,但不會封鎖網路要求,因此使用者能更快收到內容。

下列是實際運作影片,服務工作處理程序會使用 And-loop 給服務工作人員的刻意啟動 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 事件需要使用,所以建議您在 Service Worker 的 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 的要求會延遲服務工作處理程序的啟動時間。我們也可以使用導覽預先載入來修正這個問題,但在這個範例中,我們不想預先載入整個網頁,但想要預先載入 include。

為支援這項功能,系統會在每個預先載入要求中傳送標頭:

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 協助開發這項功能並提供這篇文章。感謝所有參與標準化工作的玩家

新可互通系列的一部分