瀏覽器快取機制剖析
“快取一直是前端優化的主戰場,利用好快取就成功了一半。本篇從HTTP請求和響應的頭域入手,讓你對瀏覽器快取有個整體的概念。最終你會發現強快取,協商快取 和 啟發式快取是如此的簡單。 ”
導讀
瀏覽器對於請求資源,擁有一系列成熟的快取策略。按照發生的時間順序分別為儲存策略、過期策略、協商策略,其中儲存策略在收到響應後應用,過期策略、協商策略在傳送請求前應用。流程圖如下所示:
廢話不多說,我們先來看兩張表格。
1. HTTP Header中與快取有關的Key:
key |
描述 |
儲存策略 |
過期策略 |
協商策略 |
---|---|---|---|---|
Cache-Control |
指定快取機制,覆蓋其它設定 |
✔️ |
✔️ |
|
Pragma |
http1.0欄位,指定快取機制 |
✔️ |
||
Expires |
http1.0欄位,指定快取的過期時間 |
✔️ |
||
Last-Modified |
資源最後一次的修改時間 |
✔️ |
||
ETag |
唯一標識請求資源的字串 |
✔️ |
2. 快取協商策略用於重新驗證快取資源是否有效,有關的Key如下:
key |
描述 |
---|---|
If-Modified-Since |
快取校驗欄位,值為資源最後一次的修改時間,即上次收到的Last-Modified值 |
If-Unmodified-Since |
同上,處理方式與之相反 |
If-Match |
快取校驗欄位,值為唯一標識請求資源的字串,即上次收到的ETag值 |
If-None-Match |
同上,處理方式與之相反 |
各個頭域(key)的作用
Cache-Control
瀏覽器快取裡,Cache-Control是金字塔頂尖的規則,它藐視一切其他設定,只要其他設定與其牴觸,一律覆蓋之。
不僅如此,它還是一個複合規則,包含多種值,橫跨儲存策略、過期策略兩種,同時在請求頭和響應頭都可設定。
語法為: “Cache-Control : cache-directive”。
Cache-directive共有如下12種(其中請求中指令7種,響應中指令9種):
Cache-directive |
描述 |
儲存策略 |
過期策略 |
請求欄位 |
響應欄位 |
---|---|---|---|---|---|
public |
資源將被客戶端和代理伺服器快取 |
✔️ |
✔️ |
||
private |
資源僅被客戶端快取,代理伺服器不快取 |
✔️ |
✔️ |
||
no-store |
請求和響應都不快取 |
✔️ |
✔️ |
✔️ |
|
no-cache |
相當於max-age:0,must-revalidate即資源被快取,但是快取立刻過期,同時下次訪問時強制驗證資源有效性 |
✔️ |
✔️ |
✔️ |
✔️ |
max-age |
快取資源,但是在指定時間(單位為秒)後快取過期 |
✔️ |
✔️ |
✔️ |
✔️ |
s-maxage |
同上,依賴public設定,覆蓋max-age,且只在代理伺服器上有效。 |
✔️ |
✔️ |
✔️ |
|
max-stale |
指定時間內,即使快取過時,資源依然有效 |
✔️ |
✔️ |
||
min-fresh |
快取的資源至少要保持指定時間的新鮮期 |
✔️ |
✔️ |
||
must-revalidation / proxy-revalidation |
如果快取失效,強制重新向伺服器(或代理)發起驗證(因為max-stale等欄位可能改變快取的失效時間) |
✔️ |
✔️ |
||
only-if-cached |
僅僅返回已經快取的資源,不訪問網路,若無快取則返回504 |
✔️ |
|||
no-transform |
強制要求代理伺服器不要對資源進行轉換,禁止代理伺服器對 Content-Encoding,Content-Range,Content-Type欄位的修改(因此代理的gzip壓縮將不被允許) |
✔️ |
✔️ |
假設所請求資源於4月5日快取,且在4月12日過期。
當max-age 與 max-stale 和 min-fresh 同時使用時,它們的設定相互之間獨立生效,最為保守的快取策略總是有效。這意味著,如果max-age=10 days,max-stale=2 days,min-fresh=3 days,那麼:
- 根據max-age的設定,覆蓋原快取週期, 快取資源將在4月15日失效(5+10=15);
- 根據max-stale的設定,快取過期後兩天依然有效,此時響應將返回110(Response is stale)狀態碼,快取資源將在4月14日失效(12+2=14);
- 根據min-fresh的設定,至少要留有3天的新鮮期,快取資源將在4月9日失效(12-3=9);
由於客戶端總是採用最保守的快取策略,因此,4月9日後,對於該資源的請求將重新向伺服器發起驗證。
Pragma
HTTP1.0欄位,通常設定為Pragma:no-cache,作用同Cache-Control:no-cache。當一個no-cache請求傳送給一個不遵循HTTP/1.1的伺服器時,客戶端應該包含pragma指令。為此,勾選☑️ 上disable cache時,瀏覽器自動帶上了pragma欄位。如下:
Expires
Expires:Wed, 05 Apr 2017 00:55:35 GMT
即到期時間,以伺服器時間為參考系,其優先順序比 Cache-Control:max-age 低,兩者同時出現在響應頭時,Expires將被後者覆蓋。如果Expires,Cache-Control: max-age,或 Cache-Control:s-maxage 都沒有在響應頭中出現,並且也沒有其它快取的設定,那麼瀏覽器預設會採用一個啟發式的演算法,通常會取響應頭的Date_value - Last-Modified_value值的10%作為快取時間。
如下資源便採取了啟發式快取演算法。
其快取時間為 `(Date_value - Last-Modified_value) * 10%,計算如下:
可見該資源將於2017年4月18日23點25分41秒過期,嘗試以下兩步進行驗證:
1) 試著把本地時間修改為2017年4月18日23點25分40秒,迅速重新整理頁面,發現強快取依然有效(依舊是200 OK (from disk cache));
2) 然後又修改本地時間為2017年4月18日23點26分40秒(即往後撥1分鐘),重新整理頁面,發現快取已過期,此時瀏覽器重新向伺服器發起了驗證,且命中了304協商快取,如下所示:
3) 將本地時間恢復正常(即 2017-04-06 09:54:19)。重新整理頁面,發現Date依然是4月18日,如下所示:
從⚠️ Provisional headers are shown 和Date欄位可以看出來,瀏覽器並未發出請求,快取依然有效,只不過此時Status Code顯示為200 OK。(甚至我還專門打開了charles,也沒有發現該資源的任何請求,可見這個200 OK多少有些誤導人的意味)
可見,啟發式快取演算法採用的快取時間可長可短,因此對於常規資源,建議明確設定快取時間(如指定max-age 或 expires)。
ETag
ETag:"fcb82312d92970bdf0d18a4eca08ebc7efede4fe"
實體標籤,伺服器資源的唯一識別符號,瀏覽器可以根據ETag值快取資料,節省頻寬。如果資源已經改變,etag可以幫助防止同步更新資源的相互覆蓋。ETag 優先順序比 Last-Modified 高。
If-Match
語法:If-Match: ETag_value 或者 If-Match: ETag_value, ETag_value, …
快取校驗欄位,其值為上次收到的一個或多個etag 值。常用於判斷條件是否滿足,如下兩種場景:
- 對於 GET 或 HEAD 請求,結合 Range 頭欄位,它可以保證新範圍的請求和前一個來自相同的源,如果不匹配,伺服器將返回一個416(Range Not Satisfiable)狀態碼的響應。
- 對於 PUT 或者其他不安全的請求,If-Match 可用於阻止錯誤的更新操作,如果不匹配,伺服器將返回一個412(Precondition Failed)狀態碼的響應。
If-None-Match
語法:If-None-Match: ETag_value 或者 If-None-Match: ETag_value, ETag_value, …
快取校驗欄位,結合ETag欄位,常用於判斷快取資源是否有效,優先順序比If-Modified-Since高。
- 對於 GET 或 HEAD 請求,如果其etags列表均不匹配,伺服器將返回200狀態碼的響應,反之,將返回304(Not Modified)狀態碼的響應。無論是200還是304響應,都至少返回 Cache-Control,Content-Location,Date,ETag,Expires,and Vary 中之一的欄位。
- 對於其他更新伺服器資源的請求,如果其etags列表匹配,伺服器將執行更新,反之,將返回412(Precondition Failed)狀態碼的響應。
Last-Modified
語法:Last-Modified: 星期,日期 月份 年份 時:分:秒 GMT
用於標記請求資源的最後一次修改時間,格式為GMT(格林尼治標準時間)。如可用 new Date().toGMTString()獲取當前GMT時間。Last-Modified 是 ETag 的fallback機制,優先順序比 ETag 低,且只能精確到秒,因此不太適合短時間內頻繁改動的資源。不僅如此,伺服器端的靜態資源,通常需要編譯打包,可能出現資源內容沒有改變,而Last-Modified卻改變的情況。
If-Modified-Since
語法同上,如:
快取校驗欄位,其值為上次響應頭的Last-Modified值,若與請求資源當前的Last-Modified值相同,那麼將返回304狀態碼的響應,反之,將返回200狀態碼響應。
If-Unmodified-Since
快取校驗欄位,語法同上。表示資源未修改則正常執行更新,否則返回412(Precondition Failed)狀態碼的響應。常用於如下兩種場景:
- 不安全的請求,比如說使用post請求更新wiki文件,文件未修改時才執行更新。
- 與 If-Range 欄位同時使用時,可以用來保證新的片段請求來自一個未修改的文件。
強快取
一旦資源命中強快取,瀏覽器便不會向伺服器傳送請求,而是直接讀取快取。Chrome下的現象是 200 OK (from disk cache) 或者 200 OK (from memory cache)。如下:
對於常規請求,只要存在該資源的快取,且Cache-Control:max-age 或者expires沒有過期,那麼就能命中強快取。
協商快取
快取過期後,繼續請求該資源,對於現代瀏覽器,擁有如下兩種做法:
- 根據上次響應中的ETag_value,自動往request header中新增If-None-Match欄位。伺服器收到請求後,拿If-None-Match欄位的值與資源的ETag值進行比較,若相同,則命中協商快取,返回304響應。
- 根據上次響應中的Last-Modified_value,自動往request header中新增If-Modified-Since欄位。伺服器收到請求後,拿If-Modified-Since欄位的值與資源的Last-Modified值進行比較,若相同,則命中協商快取,返回304響應。
以上,ETag優先順序比Last-Modified高,同時存在時,前者覆蓋後者。下面通過例項來理解下強快取和協商快取。
如下忽略首次訪問,第二次通過 If-Modified-Since 命中了304協商快取。
協商快取的響應結果,不僅驗證了資源的有效性,同時還更新了瀏覽器快取。主要更新內容如下:
Age:0 表示命中了代理伺服器的快取,age值為0表示代理伺服器剛剛重新整理了一次快取。
Cache-Control:max-age=600 覆蓋 Expires 欄位,表示從Date_value,即 Wed, 05 Apr 2017 13:09:36 GMT 起,10分鐘之後快取過期。因此10分鐘之內訪問,將會命中強快取,如下所示:
當然,除了上述與快取直接相關的欄位外,http header中還包括如下間接相關的欄位。
Age
出現此欄位,表示命中代理伺服器的快取。它指的是代理伺服器對於請求資源的已快取時間,單位為秒。如下:
Age:2383321
Date:Wed, 08 Mar 2017 16:12:42 GMT
以上指的是,代理伺服器在2017年3月8日16:12:42時向源伺服器發起了對該資源的請求,目前已快取了該資源2383321秒。
Date
指的是響應生成的時間。請求經過代理伺服器時,返回的Date未必是最新的,通常這個時候,代理伺服器將增加一個Age欄位告知該資源已快取了多久。
Vary
對於伺服器而言,資原始檔可能不止一個版本,比如說壓縮和未壓縮,針對不同的客戶端,通常需要返回不同的資源版本。比如說老式的瀏覽器可能不支援解壓縮,這個時候,就需要返回一個未壓縮的版本; 對於新的瀏覽器,支援壓縮,返回一個壓縮的版本,有利於節省頻寬,提升體驗。那麼怎麼區分這個版本呢,這個時候就需要Vary了。
伺服器通過指定Vary: Accept-Encoding,告知代理伺服器,對於這個資源,需要快取兩個版本: 壓縮和未壓縮。這樣老式瀏覽器和新的瀏覽器,通過代理,就分別拿到了未壓縮和壓縮版本的資源,避免了都拿同一個資源的尷尬。
Vary:Accept-Encoding,User-Agent
如上設定,代理伺服器將針對是否壓縮和瀏覽器型別兩個維度去快取資源。如此一來,同一個url,就能針對PC和Mobile返回不同的快取內容。
怎麼讓瀏覽器不快取靜態資源
實際上,工作中很多場景都需要避免瀏覽器快取,除了瀏覽器隱私模式,請求時想要禁用快取,還可以設定請求頭: Cache-Control: no-cache, no-store, must-revalidate 。
當然,還有一種常用做法: 即給請求的資源增加一個版本號,如下:
這樣做的好處就是你可以自由控制什麼時候載入最新的資源。
不僅如此,HTML也可以禁用快取,即在頁面的
上述雖能禁用快取,但只有部分瀏覽器支援,而且由於代理不解析HTML文件,故代理伺服器也不支援這種方式。
IE8的異常表現
實際上,上述快取有關的規律,並非所有瀏覽器都完全遵循,比如IE8。
資源快取是否有效相關。
瀏覽器 |
前提 |
操作 |
表現 |
正常表現 |
---|---|---|---|---|
IE8 |
資源快取有效 |
新開一個視窗載入網頁 |
重新發送請求(返回200) |
展示快取的頁面 |
IE8 |
資源快取失效 |
原瀏覽器視窗中單擊 Enter 按鈕 |
展示快取的頁面 |
重新發送請求(返回200) |
Last-Modified / E-Tag 相關。
瀏覽器 |
前提 |
操作 |
表現 |
正常表現 |
---|---|---|---|---|
IE8 |
資源內容沒有修改 |
新開一個視窗載入網頁 |
瀏覽器重新發送請求(返回200) |
重新發送請求(返回304) |
IE8 |
資源內容已修改 |
原瀏覽器視窗中單擊 Enter 按鈕 |
瀏覽器展示快取的頁面 |
重新發送請求(返回200) |
本問就討論這麼多內容,有什麼問題或好的想法歡迎點選「閱讀原文」參與留言和評論。
參考文章
- Cache Policy Interaction—Maximum Age and Maximum Staleness
- HTTP/1.1: Header Field Definitions
- http - What’s the difference between Cache-Control: max-age=0 and no-cache? - Stack Overflow
- App 快取方案:Http 快取 · baitouwei
- Cache-Control - HTTP | MDN
- 徹底弄懂 Http 快取機制 - 基於快取策略三要素分解法