瀏覽器快取機制剖析

路易斯發表於2017-04-10

快取一直是前端優化的主戰場, 利用好快取就成功了一半. 本篇從http請求和響應的頭域入手, 讓你對瀏覽器快取有個整體的概念. 最終你會發現強快取, 協商快取 和 啟發式快取是如此的簡單.

導讀

我不知道拖延症是有多嚴重, 反正去年3月開的題, 直到今年4月才開始寫.(請盡情吐槽吧)

瀏覽器對於請求資源, 擁有一系列成熟的快取策略. 按照發生的時間順序分別為儲存策略, 過期策略, 協商策略, 其中儲存策略在收到響應後應用, 過期策略, 協商策略在傳送請求前應用. 流程圖如下所示.

瀏覽器快取機制剖析

廢話不多說, 我們先來看兩張表格.

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欄位. 如下:

瀏覽器快取機制剖析
Pragma:no-cache

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%, 計算如下:

const Date_value = new Date('Thu, 06 Apr 2017 01:30:56 GMT').getTime();
const LastModified_value = new Date('Thu, 01 Dec 2016 06:23:23 GMT').getTime();
const cacheTime = (Date_value - LastModified_value) / 10;
const Expires_timestamp = Date_value + cacheTime;
const Expires_value = new Date(Expires_timestamp);
console.log('Expires:', Expires_value); // Expires: Tue Apr 18 2017 23:25:41 GMT+0800 (CST)複製程式碼

可見該資源將於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協商快取, 如下所示.

瀏覽器快取機制剖析
快取過期, 重新發起驗證, 命中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

Last-Modified: Tue, 04 Apr 2017 10:01:15 GMT複製程式碼

用於標記請求資源的最後一次修改時間, 格式為GMT(格林尼治標準時間). 如可用 new Date().toGMTString()獲取當前GMT時間. Last-Modified 是 ETag 的fallback機制, 優先順序比 ETag 低, 且只能精確到秒, 因此不太適合短時間內頻繁改動的資源. 不僅如此, 伺服器端的靜態資源, 通常需要編譯打包, 可能出現資源內容沒有改變, 而Last-Modified卻改變的情況.

If-Modified-Since

語法同上, 如:

If-Modified-Since: Tue, 04 Apr 2017 10:12:27 GMT複製程式碼

快取校驗欄位, 其值為上次響應頭的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). 如下:

瀏覽器快取機制剖析
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協商快取.

瀏覽器快取機制剖析
304

協商快取的響應結果, 不僅驗證了資源的有效性, 同時還更新了瀏覽器快取. 主要更新內容如下:

Age:0
Cache-Control:max-age=600
Date: Wed, 05 Apr 2017 13:09:36 GMT
Expires:Wed, 05 Apr 2017 00:55:35 GMT複製程式碼

Age:0 表示命中了代理伺服器的快取, age值為0表示代理伺服器剛剛重新整理了一次快取.

Cache-Control:max-age=600 覆蓋 Expires 欄位, 表示從Date_value, 即 Wed, 05 Apr 2017 13:09:36 GMT 起, 10分鐘之後快取過期. 因此10分鐘之內訪問, 將會命中強快取, 如下所示:

瀏覽器快取機制剖析
200 from cache

當然, 除了上述與快取直接相關的欄位外, 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 .

當然, 還有一種常用做法: 即給請求的資源增加一個版本號, 如下:

<link rel="stylesheet" type="text/css" href="../css/style.css?version=1.8.9"/>複製程式碼

這樣做的好處就是你可以自由控制什麼時候載入最新的資源.

不僅如此, HTML也可以禁用快取, 即在頁面的\

節點中加入\標籤, 程式碼如下:
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"/>複製程式碼

上述雖能禁用快取, 但只有部分瀏覽器支援, 而且由於代理不解析HTML文件, 故代理伺服器也不支援這種方式.

IE8的異常表現

實際上, 上述快取有關的規律, 並非所有瀏覽器都完全遵循. 比如說IE8.

資源快取是否有效相關.

瀏覽器 前提 操作 表現 正常表現
IE8 資源快取有效 新開一個視窗載入網頁 重新傳送請求(返回200) 展示快取的頁面
IE8 資源快取失效 原瀏覽器視窗中單擊 Enter 按鈕 展示快取的頁面 重新傳送請求(返回200)

Last-Modified / E-Tag 相關.

瀏覽器 前提 操作 表現 正常表現
IE8 資源內容沒有修改 新開一個視窗載入網頁 瀏覽器重新傳送請求(返回200) 重新傳送請求(返回304)
IE8 資源內容已修改 原瀏覽器視窗中單擊 Enter 按鈕 瀏覽器展示快取的頁面 重新傳送請求(返回200)

本問就討論這麼多內容,大家有什麼問題或好的想法歡迎在下方參與留言和評論.

本文作者: louis

本文連結: louiszhai.github.io/2017/04/07/…

參考文章

相關文章