雖然 ServiceWorker 和 PWA 正在成為現代 Web 應用程式的標準,但瀏覽器資源快取變得比以往任何時候都複雜。
本文涵蓋了瀏覽器快取的重點內容,具體包括:
- ServiceWorker 快取與 HTTP 快取的優先順序?
- 主流瀏覽器實現的 MemoryCache 和 DiskCache 在哪一層?
- MemoryCache、DiskCache、ServiceWorker 快取哪個速度更快?
快取流程概述
我們先來看標準定義的資源請求遵循的順序:
- ServiceWorker 快取:ServiceWorker 檢查資源是否存在其快取中,並根據其程式設計的快取策略決定是否返回資源。這個操作不會自動發生,需要在註冊的 ServiceWorker 中定義
fetch
事件去攔截並處理網路請求,這樣才能命中 ServiceWorker 快取而不是網路或者 HTTP 快取。 - HTTP 快取:這裡就是我們常常說的「強快取」和「協商快取」,如果 HTTP 快取未過期的話,瀏覽器就會使用 HTTP 快取的資源。
- 伺服器端:如果 ServiceWorker 快取或者 HTTP 快取中未找到任何資源,則瀏覽器會向網路請求資源。這裡就會涉及到 CDN 服務或者源服務的工作了。
這是標準定義的資源請求流程,但是有追求的瀏覽器還會在 ServiceWorker 上面加一層 「記憶體快取層」 ,以 Chrome 為例,我們請求一個資源,除去網路,會有三種瀏覽器快取返回:
那麼 MemoryCache 和 DiskCache 與 ServiceWorker Cache 的優先順序是怎麼樣的呢?
下面我們講講三者的區別。
MemoryCache、DiskCache 在快取流程的哪一層?
我們以 Chrome 為例,MemoryCache 作為第一公民,位於 ServiceWorker 之上。
也就是命中了 MemoryCache,就不會觸發 ServiceWorker 的 fetch 事件。
而 DiskCache 則位於原來的 HTTP 快取層:
MemoryCache 的存在會導致一個問題: ServiceWorker 並不總是對資源有著控制權。
這會另我們本來期望的情況會變得複雜且不可預知。可惜的是 MemoryCache 並不在 W3C 的標準中,W3C 從 2016 年到現在仍然在討論著這個事情,看來短時間這個問題是得不到解決了。
一些正在討論的話題:
我們真的沒有辦法麼?
要是我們遇到業務場景,確實對 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 快取哪個速度更快?
我們再看一下同一個資源三種快取的載入速度和優先順序:
- 載入速度:MemoryCache > DiskCache > ServiceWorker
- 優先順序:MemoryCache > ServiceWorker> DiskCache
MemoryCache 優先順序在 ServiceWorker 前面,這個沒問題。
但是速度更慢的 ServiceWorker 優先順序比速度更快的 DiskCache 更高?
那盤下來,ServiceWorker 豈不是減慢了站點的載入速度?
對照實驗
為了研究這個問題,我做了一組對照實驗。
實驗只在 Chrome 進行,chrome devtool 為每個資源提供時間。所有載入資源的資訊都可以作為 HAR 檔案下載下來,然後編寫本地指令碼進行資訊提取和分析。
實驗條件
- 同一個環境:Chrome97 / MacOS 10.14 / Wifi
同一張圖片的多次併發載入:
- 3 張 133KB 圖片 10 次實驗
- 10 張 133KB 圖片 10 次實驗
- 100 張 133KB 圖片 10 次實驗
觀察兩個效能:
- DiskCache 快取效能表現
- ServiceWorker 快取速度表現
實驗一:3 張 133KB 圖片併發
首先是併發請求 3 張圖片進行 10 次實驗,取平均資料,然後分別觀察 DiskCache、ServiceWorker Cache 的效能表現。
觀察:
- DiskCache:我們發現下載操作並沒有花太多時間,但是資源在等待排隊。
- ServiceWorker Cache:更多耗時在下載。
結論:但儘管如此,這種情況下, DiskCache 依然是比 ServiceWorker Cache 更快。
實驗二:3 張 133KB 圖片 10 次實驗
當我把併發圖片增加到 10 張,這種情況可能會更加接近於實際情況,站點中可能會擁有更多的不同的資源(JS檔案、字型、樣式、影像等),因為某些網站可能會在一個頁面存在超過 10 個資源。
觀察:
- DiskCache:從第二個資源開始排隊時間依然很長,但是下載時間是基本不變的。
- ServiceWorker Cache: 排隊並不是問題,但等待是。
結論:這種情況下, DiskCache 會略遜於 ServiceWorker Cache。
實驗三:3 張 133KB 圖片 100 次實驗
當我把併發圖片增加到 100 張,這種情況幾乎是不真實的情況,但是我好奇為什麼 DiskCache 為什麼在第一次試驗中比 ServiceWorker Cache 更快。
觀察:
- DiskCache:排隊依然是問題,且隨著併發數成線性上升。我們甚至能看到瀏覽器是如何載入圖片的,一次併發大概 6 張圖片。
- 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 張圖片
當我們進行資源競速之後,這種情況下,無論是併發少量資源還是大量資源,都能達到最快的級別。
總結
本篇我們搞懂了 ServiceCache、MemoryCache、DiskCache 的優先順序。
然後深入對比了 ServiceWorker Cache 和 DiskCache 的效能表現。
在少量資源併發的時候,DiskCache 更快,在大量資源併發的時候,ServiceWorker 更快。
最後通過「資源競速」的方式來兼顧兩種情況。
但是,在某些時候,我們比較 ServiceWorker 和 HTTP 快取有點不公平。
ServiceWorker 的用途會更加廣泛,它提供了更細力度的快取控制、使離線化應用得以實現、並且對比主執行緒,他能夠使用更多的 CacheAPI 容量。