ServiceWorker 快取與 HTTP 快取

JerryC發表於2022-04-27

雖然 ServiceWorker 和 PWA 正在成為現代 Web 應用程式的標準,但瀏覽器資源快取變得比以往任何時候都複雜。
本文涵蓋了瀏覽器快取的重點內容,具體包括:

  1. ServiceWorker 快取與 HTTP 快取的優先順序?
  2. 主流瀏覽器實現的 MemoryCache 和 DiskCache 在哪一層?
  3. MemoryCache、DiskCache、ServiceWorker 快取哪個速度更快?

快取流程概述

我們先來看標準定義的資源請求遵循的順序:

快取流

  1. ServiceWorker 快取:ServiceWorker 檢查資源是否存在其快取中,並根據其程式設計的快取策略決定是否返回資源。這個操作不會自動發生,需要在註冊的 ServiceWorker 中定義 fetch 事件去攔截並處理網路請求,這樣才能命中 ServiceWorker 快取而不是網路或者 HTTP 快取。
  2. HTTP 快取:這裡就是我們常常說的「強快取」和「協商快取」,如果 HTTP 快取未過期的話,瀏覽器就會使用 HTTP 快取的資源。
  3. 伺服器端:如果 ServiceWorker 快取或者 HTTP 快取中未找到任何資源,則瀏覽器會向網路請求資源。這裡就會涉及到 CDN 服務或者源服務的工作了。

這是標準定義的資源請求流程,但是有追求的瀏覽器還會在 ServiceWorker 上面加一層 「記憶體快取層」 ,以 Chrome 為例,我們請求一個資源,除去網路,會有三種瀏覽器快取返回:

image-20220420193610795

那麼 MemoryCache 和 DiskCache 與 ServiceWorker Cache 的優先順序是怎麼樣的呢?
下面我們講講三者的區別。

MemoryCache、DiskCache 在快取流程的哪一層?

我們以 Chrome 為例,MemoryCache 作為第一公民,位於 ServiceWorker 之上。
也就是命中了 MemoryCache,就不會觸發 ServiceWorker 的 fetch 事件。
而 DiskCache 則位於原來的 HTTP 快取層:

http-cache-priority.drawio

MemoryCache 的存在會導致一個問題: ServiceWorker 並不總是對資源有著控制權。
這會另我們本來期望的情況會變得複雜且不可預知。可惜的是 MemoryCache 並不在 W3C 的標準中,W3C 從 2016 年到現在仍然在討論著這個事情,看來短時間這個問題是得不到解決了。

一些正在討論的話題:

  1. safari fetches from memory cache instead of Service worker
  2. Difference between disk and memory cache
  3. Advanced Questions About Service Worker
  4. allow service worker produced resources to be marked as "cachable"

我們真的沒有辦法麼?

要是我們遇到業務場景,確實對 ServiceWorker 資源控制權有很強的的要求,我們還是可以做點事情的。
MemoryCache 是受控於 「強快取」 的,這意味著我們可以在 ServiceWorker 攔截資源的響應,並設定資源響應頭來使資源從 MemoryCache 失效:

cache-control: max-age=0
self.addEventListener("fetch", (event) => {
  event.respondWith(
    (async function () {
      // 從 HTTP 快取或者網路獲取資源
      const res =  fetch(event.request);
      
      // 因為 Response 是一個流,只能用一次,所以這裡要 clone 一下。
      const newRes = res.clone();
      
      // 改寫資源響應頭
            return new Response(res.body, { ...newRes, headers: {
        'cache-control': 'max-age=0'
      }});
    })();
  );
});

需要注意的是,這種方法是以犧牲少量載入效能為前提的。這取決於我們實際場景中是效能優先,還是離線優先,或者其他什麼情況優先。

MemoryCache、DiskCache、ServiceWorker 快取哪個速度更快?

image-20220420203359745

我們再看一下同一個資源三種快取的載入速度和優先順序:

  1. 載入速度:MemoryCache > DiskCache > ServiceWorker
  2. 優先順序:MemoryCache > ServiceWorker> DiskCache

MemoryCache 優先順序在 ServiceWorker 前面,這個沒問題。
但是速度更慢的 ServiceWorker 優先順序比速度更快的 DiskCache 更高?
那盤下來,ServiceWorker 豈不是減慢了站點的載入速度?

對照實驗

為了研究這個問題,我做了一組對照實驗。
實驗只在 Chrome 進行,chrome devtool 為每個資源提供時間。所有載入資源的資訊都可以作為 HAR 檔案下載下來,然後編寫本地指令碼進行資訊提取和分析。

image-20220421001201494

實驗條件

  1. 同一個環境:Chrome97 / MacOS 10.14 / Wifi
  2. 同一張圖片的多次併發載入:

    1. 3 張 133KB 圖片 10 次實驗
    2. 10 張 133KB 圖片 10 次實驗
    3. 100 張 133KB 圖片 10 次實驗
  3. 觀察兩個效能:

    1. DiskCache 快取效能表現
    2. ServiceWorker 快取速度表現

實驗一:3 張 133KB 圖片併發

image-20220421013805667

首先是併發請求 3 張圖片進行 10 次實驗,取平均資料,然後分別觀察 DiskCache、ServiceWorker Cache 的效能表現。

觀察:

  1. DiskCache:我們發現下載操作並沒有花太多時間,但是資源在等待排隊。
  2. ServiceWorker Cache:更多耗時在下載。

結論:但儘管如此,這種情況下, DiskCache 依然是比 ServiceWorker Cache 更快。

實驗二:3 張 133KB 圖片 10 次實驗

image-20220421013924399

當我把併發圖片增加到 10 張,這種情況可能會更加接近於實際情況,站點中可能會擁有更多的不同的資源(JS檔案、字型、樣式、影像等),因為某些網站可能會在一個頁面存在超過 10 個資源。

觀察:

  1. DiskCache:從第二個資源開始排隊時間依然很長,但是下載時間是基本不變的。
  2. ServiceWorker Cache: 排隊並不是問題,但等待是。

結論:這種情況下, DiskCache 會略遜於 ServiceWorker Cache。

實驗三:3 張 133KB 圖片 100 次實驗

image-20220421014006376

當我把併發圖片增加到 100 張,這種情況幾乎是不真實的情況,但是我好奇為什麼 DiskCache 為什麼在第一次試驗中比 ServiceWorker Cache 更快。

觀察:

  1. DiskCache:排隊依然是問題,且隨著併發數成線性上升。我們甚至能看到瀏覽器是如何載入圖片的,一次併發大概 6 張圖片。
  2. ServiceWorker Cache:雖然等待時間隨著併發數上升,但是是平緩的。

結論: 大併發下 ServiceWorker Cache 比 DiskCache 更快。

那 DiskCache 和 ServiceWorker 怎麼選擇?
小孩子才做選擇,大人都要

由於 ServiceWorker 的優先順序在 DiskCache 之上,我們可以在 ServiceWorker 進行 「資源競速」,同一時間請求 ServiceWorker Cache 和 DiskCache,哪個先返回就把資源返回上一層。程式碼可能是這樣的:

self.addEventListener("fetch", (event) => {
  event.respondWith(
    (async function () {
      const res = Promise.race([
        // 請求 ServiceWorker Cache
        cache.open(CACHE_NAME).then(cache => cache.match(event.request)),
        // 請求 DiskCache 或者網路資源
        fetch(event.request)
      ])
    })();
  );
});

實驗四:資源競速之後,併發請求 3 張圖片、10 張圖片 和 100 張圖片

當我們進行資源競速之後,這種情況下,無論是併發少量資源還是大量資源,都能達到最快的級別。

image-20220421014045092
image-20220421005715401

總結

本篇我們搞懂了 ServiceCache、MemoryCache、DiskCache 的優先順序。
然後深入對比了 ServiceWorker Cache 和 DiskCache 的效能表現。
在少量資源併發的時候,DiskCache 更快,在大量資源併發的時候,ServiceWorker 更快。
最後通過「資源競速」的方式來兼顧兩種情況。
但是,在某些時候,我們比較 ServiceWorker 和 HTTP 快取有點不公平。
ServiceWorker 的用途會更加廣泛,它提供了更細力度的快取控制、使離線化應用得以實現、並且對比主執行緒,他能夠使用更多的 CacheAPI 容量。

相關文章