
歡迎來到「前端性能優(yōu)化之旅」的第一站 —— 緩存。
當瀏覽器想要獲取遠程的數據時,我們的性能之旅就開始了。然而,我們并不會立即動身(發(fā)送請求)。在計算機領域,很多性能問題都會通過增加緩存來解決,前端也不例外。和許多后端服務一樣,前端緩存也是多級的。下面讓我們一起來具體看一看。
1. 本地數據存儲
通過結合本地存儲,可以在業(yè)務代碼側實現緩存。
對于一些請求,我們可以直接在業(yè)務代碼側進行緩存處理。緩存方式包括 localStorage、sessionStorage、indexedDB。把這塊加入緩存的討論也許會有爭議,但利用好它確實能在程序側達到一些類似緩存的能力。
例如,我們的頁面上有一個日更新的榜單,我們可以做一個當日緩存:
// 當用戶加載站點中的榜單組件時,可以通過該方法獲取榜單數據
async function readListData() {
const info = JSON.parse(localStorage.getItem('listInfo'));
if (isExpired(info.time, +(new Date))) {
const list = await fetchList();
localStorage.setItem('listInfo', JSON.stringify({
time: +(new Date),
list: list
}));
return list;
}
return info.list;
}
localStorage 大家都比較了解了,indexedDB 可能會了解的更少一些。想快速了解 indexedDB 使用方式可以看這篇文章[1]。
從前端視角看,這是一種本地存儲;但如果從整個系統的維度來看,很多時候其實也是緩存鏈條中的一環(huán)。對于一些特殊的、輕量級的業(yè)務數據,可以考慮使用本地存儲作為緩存。
2. 內存緩存(Memory)
當你訪問一個頁面及其子資源時,有時候會出現一個資源被使用多次,例如圖標。由于該資源已經存儲在內存中,再去請求反而多此一舉,瀏覽器內存則是最近、最快的響應場所。

內存緩存并無明確的標準規(guī)定,它與 HTTP 語義下的緩存關聯性不大,算是瀏覽器幫我們實現的優(yōu)化,很多時候其實我們意識不到。
對內存緩存感興趣,可以在這篇文章[2]的 Memory Cache 部分進一步了解。
3. Cache API
當我們沒有命中內存緩存時,是否就開始發(fā)送請求了呢?其實不一定。
在這時我們還可能會碰到 Cache API 里的緩存,提到它就不得不提一下 Service Worker 了。它們通常都是配合使用的。
首先明確一下,這層的緩存沒有規(guī)定說該緩存什么、什么情況下需要緩存,它只是提供給了客戶端構建請求緩存機制的能力。如果你對 PWA 或者 Service Worker 很了解,應該非常清楚是怎么一回事。如果不了解也沒有關系,我們可以簡單看一下:
首先,Service Worker 是一個后臺運行的獨立線程,可以在代碼中啟用
// index.js
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js').then(function () {
// 注冊成功
});
}
之后需要處理一些 Service Worker 的生命周期事件,而其中與這里提到的緩存功能直接相關的則是請求攔截:
// sw.js
self.addEventListener('fetch', function (e) {
// 如果有cache則直接返回,否則通過fetch請求
e.respondWith(
caches.match(e.request).then(function (cache) {
return cache || fetch(e.request);
}).catch(function (err) {
console.log(err);
return fetch(e.request);
})
);
});
以上代碼會攔截所有的網絡請求,查看是否有緩存的請求內容,如果有則返回緩存,否則會繼續(xù)發(fā)送請求。與內存緩存不同,Cache API 提供的緩存可以認為是“永久性”的,關閉瀏覽器或離開頁面之后,下次再訪問仍然可以使用。
Service Worker 與 Cache API 其實是一個功能非常強大的組合,能夠實現堆業(yè)務的透明,在兼容性上也可以做成漸進支持。還是非常推薦在業(yè)務中嘗試的。當然上面代碼簡略了很多,想要進一步了解 Service Worker 和 Cache API 的使用可以看這篇文章[3]。同時推薦使用 Google 的 Workbox。
4. HTTP 緩存
如果 Service Worker 中也沒有緩存的請求信息,那么就會真正到 HTTP request 的階段了。這個時候出現的就是我們所熟知的 HTTP 緩存規(guī)范。
HTTP 有一系列的規(guī)范來規(guī)定哪些情況下需要緩存請求信息、緩存多久,而哪些情況下不能進行信息的緩存。我們可以通過相關的 HTTP 請求頭來實現緩存。
HTTP 緩存大致可以分為強緩存與協商緩存。
4.1. 強緩存
在強緩存的情況下,瀏覽器不會向服務器發(fā)送請求,而是直接從本地緩存中讀取內容,這個“本地”一般就是來源于硬盤。這也就是我們在 Chrome DevTools 上經??吹降摹竏isk cache」。

與其相關的響應頭則是 Expires 和 Cache-Control。在 Expires 上可以設置一個過期時間,瀏覽器通過將其與當前本地時間對比,判斷資源是否過期,未過期則直接從本地取即可。而 Cache-Control 則可以通過給它設置一個 max-age,來控制過期時間。例如,max-age=300 就是表示在響應成功后 300 秒內,資源請求會走強緩存。
4.2. 協商緩存
你可能也感覺到了,強緩存不是那么靈活。如果我在 300 秒內更新了資源,需要怎么通知客戶端呢?常用的方式就是通過協商緩存。
我們知道,遠程請求慢的一大原因就是報文體積較大。協商緩存就是希望能通過先“問一問”服務器資源到底有沒有過期,來避免無謂的資源下載。這伴隨的往往會是 HTTP 請求中的 304 響應碼。下面簡單介紹一下實現協商緩存的兩種方式:
一種協防緩存的方式是:服務器第一次響應時返回 Last-Modified,而瀏覽器在后續(xù)請求時帶上其值作為 If-Modified-Since,相當于問服務端:XX 時間點之后,這個資源更新了么?服務器根據實際情況回答即可:更新了(狀態(tài)碼 200)或沒更新(狀態(tài)碼 304)。
上面是通過時間來判斷是否更新,如果更新時間間隔過短,例如 1s 一下,那么使用更新時間的方式精度就不夠了。所以還有一種是通過標識 —— ETag。服務器第一次響應時返回 ETag,而瀏覽器在后續(xù)請求時帶上其值作為 If-None-Match。一般會用文件的 MD5 作為 ETag。
作為前端工程師,一定要善于應用 HTTP 緩存。如果想要了解更多關于 HTTP 緩存的內容,可以閱讀這篇文章[4]。
上面這些的各級緩存的匹配機制里,都是包含資源的 uri 的匹配,即 uri 更改后不會命中緩存。也正是如此,我們目前在前端實踐中都會把文件 HASH 加入到文件名中,避免同名文件命中緩存的舊資源。
5. Push Cache
假如很不幸,以上這些緩存你都沒有命中,那么你將會碰到最后一個緩存檢查 —— Push Cache。
Push Cache 其實是 HTTP/2 的 Push 功能所帶來的。簡言之,過去一個 HTTP 的請求連接只能傳輸一個資源,而現在你在請求一個資源的同時,服務端可以為你“推送”一些其他資源 —— 你可能在在不久的將來就會用到一些資源。例如,你在請求 www.sample.com 時,服務端不僅發(fā)送了頁面文檔,還一起推送了 關鍵 CSS 樣式表。這也就避免了瀏覽器收到響應、解析到相應位置時才會請求所帶來的延后。
不過 HTTP/2 Push Cache 是一個比較底層的網絡特性,與其他的緩存有很多不同,例如:
- 當匹配上時,并不會在額外檢查資源是否過期;
- 存活時間很短,甚至短過內存緩存(例如有文章提到,Chrome 中為 5min 左右);
- 只會被使用一次;
- HTTP/2 連接斷開將導致緩存直接失效;
- ……
如果對 HTTP/2 Push 感興趣,可以看看這篇文章[5]。
好了,到目前為止,我們可能還沒有發(fā)出一個真正的請求。這也意味著,在緩存檢查階段我們就會有很多機會將后續(xù)的性能問題扼殺在搖籃之中 —— 如果遠程請求都不必發(fā)出,又何須優(yōu)化加載性能呢?
所以,審視一下我們的應用、業(yè)務,看看哪些性能問題是可以在源頭上解決的。
不過很多時候,能通過緩存解決的問題只有一部分。所以下面我們會繼續(xù)這趟旅行,目前我們已經有了一個好的開始,不是么?
目前內容已全部更新至 ? fe-performance-journey ? 倉庫中,陸續(xù)會將內容同步到掘金上。如果希望盡快閱讀相關內容,可以直接去該倉庫中瀏覽文章。
喜歡的朋友可以 star 一下,后續(xù)也會繼續(xù)更新更多性能優(yōu)化相關的內容。
參考資料
- A quick but complete guide to IndexedDB and storing data in browsers
- A Tale of Four Caches
- PWA學習與實踐:讓你的WebApp離線可用
- 瀏覽器緩存機制:強緩存、協商緩存
- HTTP/2 push is tougher than I thought
- Caching best practices & max-age gotchas
- The Offline Cookbook (Service Worker)
- HTTP/2 ORG
- Web Caching Explained by Buying Milk at the Supermarket
- 深入理解瀏覽器的緩存機制