HTTP 緩存機制 一 二 三
Web 緩存大致可以分為:數據庫緩存、服務器端緩存(代理服務器緩存、CDN 緩存)、瀏覽器緩存。
瀏覽器緩存也包含很多內容: HTTP 緩存、indexDB、cookie、localstorage 等等。這里我們只討論 HTTP 緩存相關內容。
在具體了解 HTTP 緩存之前先來明確幾個術語:
- 緩存命中率:從緩存中得到數據的請求數與所有請求數的比率。理想狀態是越高越好。
- 過期內容:超過設置的有效時間,被標記為“陳舊”的內容。通常過期內容不能用于回復客戶端的請求,必須重新向源服務器請求新的內容或者驗證緩存的內容是否仍然準備。
- 驗證:驗證緩存中的過期內容是否仍然有效,驗證通過的話刷新過期時間。
- 失效:失效就是把內容從緩存中移除。當內容發生改變時就必須移除失效的內容。
瀏覽器緩存主要是 HTTP 協議定義的緩存機制。HTML meta 標簽,例如
- <META HTTP-EQUIV="Pragma" CONTENT="no-cache">
含義是讓瀏覽器不緩存當前頁面。但是代理服務器不解析 HTML 內容,一般應用廣泛的是用 HTTP 頭信息控制緩存。
HTTP 頭信息控制緩存
大致分為兩種:強緩存和協商緩存。強緩存如果命中緩存不需要和服務器端發生交互,而協商緩存不管是否命中都要和服務器端發生交互,強制緩存的優先級高于協商緩存。具體內容下文介紹。
匹配流程(已有緩存的情況下):

強緩存
可以理解為無須驗證的緩存策略。對強緩存來說,響應頭中有兩個字段 Expires/Cache-Control 來表明規則。
Expires
Expires 指緩存過期的時間,超過了這個時間點就代表資源過期。有一個問題是由于使用具體時間,如果時間表示出錯或者沒有轉換到正確的時區都可能造成緩存生命周期出錯。并且 Expires 是 HTTP/1.0 的標準,現在更傾向于用 HTTP/1.1 中定義的 Cache-Control。兩個同時存在時也是 Cache-Control 的優先級更高。
Cache-Control
Cache-Control 可以由多個字段組合而成,主要有以下幾個取值:
1. max-age 指定一個時間長度,在這個時間段內緩存是有效的,單位是s。例如設置 Cache-Control:max-age=31536000,也就是說緩存有效期為(31536000 / 24 / 360)天,第一次訪問這個資源的時候,服務器端也返回了 Expires 字段,并且過期時間是一年后。

在沒有禁用緩存并且沒有超過有效時間的情況下,再次訪問這個資源就命中了緩存,不會向服務器請求資源而是直接從瀏覽器緩存中取。

2. s-maxage 同 max-age,覆蓋 max-age、Expires,但僅適用于共享緩存,在私有緩存中被忽略。
3. public 表明響應可以被任何對象(發送請求的客戶端、代理服務器等等)緩存。
4. private 表明響應只能被單個用戶(可能是操作系統用戶、瀏覽器用戶)緩存,是非共享的,不能被代理服務器緩存。
5. no-cache 強制所有緩存了該響應的用戶,在使用已緩存的數據前,發送帶驗證器的請求到服務器。不是字面意思上的不緩存。
6. no-store 禁止緩存,每次請求都要向服務器重新獲取數據。
協商緩存
緩存的資源到期了,并不意味著資源內容發生了改變,如果和服務器上的資源沒有差異,實際上沒有必要再次請求。客戶端和服務器端通過某種驗證機制驗證當前請求資源是否可以使用緩存。
瀏覽器第一次請求數據之后會將數據和響應頭部的緩存標識存儲起來。再次請求時會帶上存儲的頭部字段,服務器端驗證是否可用。如果返回 304 Not Modified,代表資源沒有發生改變可以使用緩存的數據,獲取新的過期時間。反之返回 200 就相當于重新請求了一遍資源并替換舊資源。
Last-modified/If-Modified-Since
Last-modified: 服務器端資源的最后修改時間,響應頭部會帶上這個標識。第一次請求之后,瀏覽器記錄這個時間,再次請求時,請求頭部帶上 If-Modified-Since 即為之前記錄下的時間。服務器端收到帶 If-Modified-Since 的請求后會去和資源的最后修改時間對比。若修改過就返回最新資源,狀態碼 200,若沒有修改過則返回 304。

注意:如果響應頭中有 Last-modified 而沒有 Expire 或 Cache-Control 時,瀏覽器會有自己的算法來推算出一個時間緩存該文件多久,不同瀏覽器得出的時間不一樣,所以 Last-modified 要記得配合 Expires/Cache-Control 使用。
Etag/If-None-Match
由服務器端上生成的一段 hash 字符串,第一次請求時響應頭帶上 ETag: abcd,之后的請求中帶上 If-None-Match: abcd,服務器檢查 ETag,返回 304 或 200。

關于 last-modified 和 Etag 區別,已經有很多人總結過了:
- 某些服務器不能精確得到資源的最后修改時間,這樣就無法通過最后修改時間判斷資源是否更新。
- Last-modified 只能精確到秒。
- 一些資源的最后修改時間改變了,但是內容沒改變,使用 Last-modified 看不出內容沒有改變。
- Etag 的精度比 Last-modified 高,屬于強驗證,要求資源字節級別的一致,優先級高。如果服務器端有提供 ETag 的話,必須先對 ETag 進行 Conditional Request。
注意:實際使用 ETag/Last-modified 要注意保持一致性,做負載均衡和反向代理的話可能會出現不一致的情況。計算 ETag 也是需要占用資源的,如果修改不是過于頻繁,看自己的需求用 Cache-Control 是否可以滿足。
選擇 Cache-Control 的策略(摘自 Google Developers)

實際應用
回到實際應用上來,首先要明確哪些內容適合被緩存哪些不適合。
考慮緩存的內容:
- css樣式文件
- js文件
- logo、圖標
- html文件
- 可以下載的內容
一些不應該被緩存的內容:
- 業務敏感的 GET 請求
可緩存的內容又分為幾種不同的情況:
不經常改變的文件:
給 max-age 設置一個較大的值,一般設置 max-age=31536000
比如引入的一些第三方文件、打包出來的帶有 hash 后綴 css、js 文件。一般來說文件內容改變了,會更新版本號、hash 值,相當于請求另一個文件。
標準中規定 max-age 的值最大不超過一年,所以設成 max-age=31536000。至于過期內容,緩存區會將一段時間沒有使用的文件刪除掉。
有看到用對話的形式來描述這個過程,便仿照著試圖更清晰地解釋:


可能經常需要變動的文件:
Cache-Control: no-cache / max-age=0
比如入口 index.html 文件、文件內容改變但名稱不變的資源。選擇 ETag 或 Last-Modified 來做驗證,在使用緩存資源之前一定會去服務器端做驗證,命中緩存時會比第一種情況慢一點點,畢竟還要發請求進行通信。


注意: 這里只描述了最基本的思路,實際使用 HTTP 緩存需要后端配合配置,具體情況具體對待,而且各方的實現并不一定完全按照標準來的,踩踩坑更健康。