雖然 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 年到現在仍然在討論着這個事情,看來短時間這個問題是得不到解決了。
一些正在討論的話題:
- safari fetches from memory cache instead of Service worker
- Difference between disk and memory cache
- Advanced Questions About Service Worker
- 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 緩存哪個速度更快?
我們再看一下同一個資源三種緩存的加載速度和優先級:
- 加載速度: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 容量。