今天看奇舞團推了篇文章講緩存策略的,講的挺不錯,記錄一下。 原文地址就在下面。
總結:
- 緩存分為強緩存和協商緩存。其中強緩存包括
Expires
和Cache-Control
,主要是在過期策略生效時應用的緩存。弱緩存包括Last-Modified
和ETag
,是在協商策略后應用的緩存。強弱緩存之間的主要區別在于獲取資源時是否會發送請求。 -
Cache-Control
中的max-age
指令用于指定緩存過期的相對時間,優先級高于Expires
。Cache-Control
指定為no-cache
時,由于no-cache
相當于max-age:0,must-revalidate
,所以都存在時優先級也是高于Expires
。 -
no-cache
并不是指不緩存文件,no-store
才是指不緩存文件。no-cache
僅僅是表明跳過強緩存,強制進入協商策略。 - 如果
Expires
,Cache-Control: max-age
,或Cache-Control:s-maxage
都沒有在響應頭中出現,并且設置了Last-Modified
時,那么瀏覽器默認會采用一個啟發式的算法,即啟發式緩存。通常會取響應頭的Date_value - Last-Modified_value值的10%作為緩存時間
前言
眾所周知,在Web開發中,緩存很重要、很有用。但同時其也很復雜。
本文將從以下5個方面全面地介紹下緩存相關的內容。
- 緩存的判斷策略
- 必知必會的緩存基礎
- 各類緩存的優缺點
- 緩存的最佳實踐
- 小試牛刀,看看你掌握了沒有?
一、緩存的判斷策略
瀏覽器對于所請求資源的緩存處理有一套完整的機制,主要包含以下三個策略:存儲策略、過期策略、協商策略。
其中,存儲策略發生在收到請求響應后,用于決定是否緩存相應資源;過期策略發生在請求前,用于判斷緩存是否過期;協商策略發生在請求中,用于判斷緩存資源是否更新。
瀏覽器緩存判斷策略上圖中的緩存判斷流程是瀏覽器在應用緩存時完整的判斷流程。但是在瀏覽器中訪問資源的方式不同也會導致判斷流程的不同。判斷流程會根據不同方式跳過一些流程。
瀏覽器下訪問資源的方式主要有以下7種:
- (新標簽)地址欄回車
- 鏈接跳轉
- 前進、后退
- 從收藏欄打開鏈接
- (window.open)新開窗口
- 刷新(Command + R / F5)
- 強制刷新(Command + Shift + R / Ctrl + F5)
使用這7種方式訪問資源時,應用緩存的策略會有一些不同。
需要注意的是,除此之外,還有一種特殊情況。即在當前地址欄,不改變內容,直接回車,等同于刷新當前頁。但是在當前頁點擊跳轉到自身,和鏈接跳轉一致,并不會等同于刷新。
如下圖所示。通過上述8種方式訪問資源,會從不同的緩存應用判斷步驟開始。此處不做驗證,相信大家看了后面的內容,能夠自行驗證的。
本文配有測試腳本,代碼在github上。下文會按照測試腳本進行述說,使用說明見下載鏈接。驗證上述內容,可以執行
node cache-ETag+max-age.js
,會同時開啟ETag
和max-age
,然后觸發相應的動作,通過Network面板和node日志即可驗證,此處篇幅有限先不贅述。
此外,這里提一個概念,webkit資源分為主資源和派生資源。主資源是地址欄輸入的URL請求返回的資源,派生資源是主資源中所引用的JS、CSS、圖片等資源。
在Chrome下刷新時,只有主資源的緩存應用方式如上圖所示,派生資源的緩存應用方式與新標簽打開類似,會判斷緩存是否過期。強緩存生效時的區別在于新標簽打開為from disk cache
,而當前頁刷新派生資源是from memory cache
。
而在Firefox下,當前頁面刷新,所有資源都會如上圖所示。下文也會利用Chrome的這一特點在當前頁刷新,派生資源會使用緩存進行測試。不然每次都需要打開新標簽較為繁瑣。
二、必知必會的緩存基礎
HTTP中與緩存有關的字段主要有以下10個,如下表所示。為明確表示其功能及用法,下表中分別區分了存儲策略、過期策略、協商策略、請求頭、響應頭。
Key | 描述 | 存儲策略 | 過期策略 | 協商策略 | 請求頭 | 響應頭 |
---|---|---|---|---|---|---|
Expires | 指定緩存的過期時間,值為某一時刻(絕對時間)。在指定時刻后過期 | ? | ? | ? | ||
Cache-Control | 指定緩存機制 | ? | ? | ? | ? | |
Pragma | 指定緩存機制(http1.0字段) | ? | ||||
Last-Modified | 資源最后修改時間 | 乄 | ? | ? | ||
If-Modified-Since | 緩存協商校驗字段,為上次請求收到的Last-Modified的值。處理方式見下文。 | ? | ? | |||
If-Unmodified-Since | 緩存協商校驗字段,為上次請求收到的Last-Modified的值。處理方式與If-Modified-Since相反,見下文。 | ? | ? | |||
ETag | 請求資源的唯一標識字符串 | ? | ? | |||
If-Match | 緩存協商校驗字段,請求資源的唯一標識字符串,為上次請求收到的ETag的值。處理方式見下文。 | ? | ? | |||
If-None-Match | 緩存協商校驗字段,請求資源的唯一標識字符串,為上次請求收到的ETag的值。處理方式與If-Match相反,見下文。 | ? | ? |
注:乄表示半對,Last-Modified
之所以是半對,是因為有可能會觸發啟發式緩存,也會緩存文件。具體見下文。
緩存又分為強緩存和弱緩存(又稱為協商緩存)。其中強緩存包括
Expires
和Cache-Control
,主要是在過期策略生效時應用的緩存。弱緩存包括Last-Modified
和ETag
,是在協商策略后應用的緩存。強弱緩存之間的主要區別在于獲取資源時是否會發送請求。
2.1 Expires
如上所述,Expires
指定緩存的過期時間,為絕對時間,即某一時刻。參考本地時間進行比對,在指定時刻后過期。RFC 2616建議最大值不要超過1年。
Expire
頭字段是響應頭字段,格式如下:Expires: Sat Oct 20 2018 00:00:00 GMT+0800 (CST)
。
可以嘗試以下步驟進行驗證:
執行
node cache-Expires.js
,該腳本會給請求的資源設定Expires
,值為:”2018-10-20 00:00:00”。-
訪問地址
http://localhost:1030/
,開啟Network Tab,查看avatar.jpg圖片,Expires值如下所示。 -
再次刷新會看到該資源已經被緩存,size欄顯示為
(from memory cache)
。此時修改本地時間,將時間修改為“2018-10-15 00:00:00”,再刷新,會發現緩存仍然有效。Expires緩存生效 -
如果將本地時間修改為“2018-10-25 00:00:00”,再刷新,會發現圖片不再使用緩存,而是重新獲取了,因為本地時間超過了設定值。
Expires緩存過期,重新獲取
2.2 Cache-Control
Cache-Control
用于指定資源的緩存機制,可以同時在請求頭和響應頭中設定,涉及上述三個策略中的兩個策略:存儲策略、過期策略。
Cache-Control
的語法如下:Cache-Control: cache-directive[,cache-directive]
。cache-directive
為緩存指令,大小寫不敏感,共有12個與HTTP緩存標準相關,如下表所示。其中請求指令7種,響應指令9種。Cache-Control
可以設置多個緩存指令,以逗號,
分隔。
Key | 描述 | 存儲策略 | 過期策略 | 請求字段 | 響應字段 |
---|---|---|---|---|---|
可緩存性相關 | --- | --- | --- | --- | --- |
public | 資源在客戶端和代理服務器緩存 | ? | ? | ||
private | 資源僅在在客戶端緩存,代理服務器不緩存 | ? | ? | ||
no-cache | 資源被緩存,但立即過期,下次訪問時強制向服務器驗證資源有效性。相當于max-age:0,must-revalidate
|
? | ? | ? | ? |
過期相關 | --- | --- | --- | --- | --- |
max-age=<seconds> | 在請求頭中:指出客戶端不接受有效時間大于指定時間的緩存。在響應頭中:規定資源的最大新鮮時間,指定時間后過期,單位為秒。 | ? | ? | ? | ? |
s-maxage=<seconds> | 同上,但只對代理服務器生效,如果是private 緩存,會忽略該字段。會覆蓋max-age 或Expires 頭字段 |
? | ? | ? | |
max-stale=<seconds> | 指定時間內, 即使緩存過時, 資源依然有效 | ? | ? | ||
min-fresh=<seconds> | 緩存的資源至少要保持指定時間的新鮮期 | ? | ? | ||
驗證與重載相關 | --- | --- | --- | --- | --- |
must-revalidate | 使用緩存資源之前,必須先驗證狀態,并且過期資源不應該再使用。 | ? | ? | ||
proxy-revalidate | 同上,但只對代理服務器生效,如果是private 緩存,會忽略該字段。 |
? | ? | ||
其他 | --- | --- | --- | --- | --- |
no-store | 請求和響應都不緩存 | ? | ? | ? | |
only-if-cached | 僅返回已經緩存的資源,不再向服務器獲取新的內容。若無緩存則返回504 | ? | |||
no-transform | 強制要求代理服務器不要對資源進行轉換, 禁止代理服務器對 Content-Encoding, Content-Range, Content-Type字段的修改(因此代理的gzip壓縮將不被允許) | ? | ? |
2.3.1 cache-directive大小寫不敏感
如上,cache-directive指令大小寫不敏感,所以在設置Cache-Control時,指令可以不區分大小寫。不過建議統一使用小寫。驗證如下:
- 執行
node cache-directive-case-insensitive.js
,會服務端會將max-age
寫成大寫,如下Cache-Control: MAX-AGE=86400
。 - 再次請求瀏覽器會發現緩存同樣會生效。
2.3.2 在請求頭中的max-age
max-age在請求頭中的主要應用為max-age=0
表示不使用緩存。Chrome和Firefox瀏覽器下的刷新操作(Command+ R / F5)均是在請求頭上添加了max-age=0?
指令,表示不使用強緩存,但允許協商緩存(在介紹了協商緩存的Last-Modified
和ETag
之后,可以自行驗證下這一點)。
刷新時Cache-Control
為max-age=0
驗證如下:
單獨訪問圖片資源
http://localhost:1030/avatar.jpg
,開啟Network-
刷新,可在響應頭中看到上述內容。如下圖所示。(Firefox下相同,不單獨驗證,主要最開始提到的主資源和派生資源在兩個瀏覽器中表現形式的不同)。
Chrome下刷新時,請求中的max-age值
此外,經驗證,Chrome和Firefox均對max-age
>0的情況支持不好。 在Chrome下,通過
Modify Headers
插件(Chrome和Firefox下均有類似插件)給請求添加max-age=7200
。執行
node cache-max-age.js
,訪問http://localhost:1030
,先強刷保證資源更新。-
打開NetWork,查看
avatar.jpg
,刷新,會發現,資源訪問仍然走的是緩存。如果按照規范的定義應該是不生效。
max-age > 0 在Chrome/Firefox下無效
2.3.3 max-age與Expires
Cache-Control中的max-age
指令用于指定緩存過期的相對時間。資源達到指定時間后過期。該功能與Expires類似。但其優先級高于Expires,如果同時設置max-age和Expires,max-age生效,忽略Expires。驗證如下:
- 執行
node cache-max-age+Expires.js
,會同時設置Cache-Control: max-age=86400
/Expires: Mon Oct 20 2018 00:00:00 GMT+0800 (CST)
,如下所示。
同時設置max-age和Expires - 刷新,然后再把本地時間改成當前時間延后2小時(不超過20號),會發現緩存生效。(以下兩步不再附截圖,與上述示例類似)。
- 如果將時間改為兩天后(假設20號離現在大于兩天,否則結果相反),會發現緩存不再生效,因為超出了max-age的限制。
相反,可以再試一下,max-age的有效時間大于Expires的情況,會發現依然是max-age生效。
2.3.4 no-cache和no-store
還有一點需要注意的是,no-cache并不是指不緩存文件,no-store才是指不緩存文件。no-cache僅僅是表明跳過強緩存,強制進入協商策略。
2.3 Pragma
http1.0字段, 通常設置為Pragma:no-cache
, 作用與Cache-Control:no-cache
相同。當在瀏覽器進行強刷(Comand + Shift + R / Ctrl + F5)或在NetWork面板內勾選禁用緩存(Disable Caches)時,會自動帶上Pragma:no-cache
和Cache-Control:no-cache
,并且不會帶上協商策略中所涉及的信息(下面介紹的If-Modified-Since
/If-None-Match
)。這是不會使用任何緩存,重新獲取資源。如下圖所示。
2.4 Last-Modified/If-Modified-Since/If-Unmodified-Since
Last-Modified
用于標記請求資源的最后一次修改時間。語法格式為:Last-Modified: <day-name>,<day> <month> <year> <hour>:<minute>:<second> GMT
,即GMT(格林尼治標準時間)。可用 new Date().toGMTString()獲取當前GMT時間。由于Last-Modified只能精確到秒,因此不適合在一秒內多次改變的資源。
如果Expires,Cache-Control: max-age,或 Cache-Control:s-maxage都沒有在響應頭中出現,并且設置了Last-Modified
時,那么瀏覽器默認會采用一個啟發式的算法,即啟發式緩存。通常會取響應頭的Date_value - Last-Modified_value值的10%作為緩存時間。驗證如下:
- 執行
node cache-Last-Modified.js
,服務器會獲取資源的最后修改時間,設置為Last-Modified
的值。訪問localhost:1030
,查看avatar.jpg
,如下圖所示:
Last-Modified設定 - 刷新瀏覽器,會發現圖片會從緩存獲取。
- 通過啟發式緩存的公司可以計算出緩存的時間,修改本地時間超過緩存時間后,再刷新,會發現緩存失效。
2.4.1 If-Modified-Since
返回的資源帶有Last-Modified
標識時,再次請求該資源,瀏覽器會自動帶上If-Modified-Since
,值為返回的Last-Modified
值。請求到達服務器后,服務器進行判斷,如果從上次更新后沒有再更新,則返回304。如果更新了則重新返回。驗證如下:
- 執行
node cache-Last-Modified.js
,服務器會獲取資源的最后修改時間,設置為Last-Modified
的值。如下圖所示,并且注意看一下資源的大小。
Last-Modified設定
-
刷新頁面,再次查看NetWork。會發現請求頭中帶上了
If-Modified-Since
。如果服務器判斷資源未改變,則返回304,此外由于服務器返回304,資源會從緩存獲取,所以資源大小也減少了,如下所示。
304 請求資源大小 -
修改
index.html
文件的內容,再次刷新。會發現返回變成200,html內容更新了,并且返回了新的Last-Modified
的值,資源大小也相應地改變了。
修改后資源大小
304請求也可以觸發存儲策略,如文章開頭的流程判斷圖所示,可自行驗證,返回時添加相應header即可。
注意,If-Modified-Since
只能用于GET、HEAD請求。
2.4.2 If-Unmodified-Since
If-Unmodified-Since
表示資源未修改則正常執行更新,否則返回412(Precondition Failed)狀態碼的響應。主要有如下兩種場景。
- 用于不安全的請求中從而是請求具備條件性(如POST或者其他不安全的方法),如請求更新wiki文檔,文檔未修改時才執行更新。
- 與
If-Range
字段同時使用時,可以用來保證新的片段請求來自一個未修改的文檔。
2.5 ETag/If-Match/If-None-Match
ETag是請求資源在服務器的唯一標識,瀏覽器可以根據ETag值緩存數據。在再次請求時通過If-None-Match
攜帶上次的ETag值,如果值不變,則返回304,如果改變你則返回新的內容。
需要注意的是,ETag和If-None-Match的值均為雙引號包裹的。
驗證步驟與Last-Modified
相似。執行node cache-ETag.js
即可。此處不再詳述。
If-Match
判斷邏輯邏輯與If-None-Match
相反。
最后,ETag
的優先級高于Last-Modified
。當ETag
和Last-Modified
,ETag
優先級更高,但不會忽略Last-Modified
,需要服務端實現。驗證如下,其中服務端判斷優先級:
-
執行
node cache-ETag+Last-Modified.js
。服務端會在資源的響應頭中,同時設置ETag
和Last-Modified
。如下圖:
同時設置ETag和Last-Modified -
刷新瀏覽器,會發現
index.html
請求時304。查看node日志,會看到ETag生效
。如下:
三、緩存的優缺點
好了,通過長長的第二部分,我們簡單介紹了一下HTTP Cache的基礎知識。下面我再匯總一下各類緩存之間的優缺點吧。如下表所示:
緩存頭部 | 優點 | 缺點 |
---|---|---|
Expires | 1. HTTP 1.0 產物,可以在HTTP 1.0和1.1中使用。2. 簡單易用,通過絕對時間標識失效時間。 | 1. 時間為服務器返回的時間,如果本地時間與服務器時間不一致,則可能會出現問題。(如上述我們通過修改本地時間是緩存失效。)2. 存在版本問題,在資源過期之前如果對資源進行修改,客戶端都是無法獲知的。 |
Cache-Control | 1. HTTP 1.1的內容,以相對時間標識失效時間,解決了Expires服務器和客戶端相對時間的問題。2. 支持的指令較多,可以根據需要進行相應的配置。 | 1. HTTP 1.1 才有的內容,不適用于HTTP 1.0 。2. 與Expires類似,存在版本問題,在資源過期之前如果對資源進行修改,客戶端都是無法獲知的。 |
Last-Modified | 1. 不存在版本問題,每次都會跟服務器進行校驗,符合則304不返回資源,不符合則重新返回資源。 | 1. 以時刻作為標識,精確到秒,無法識別一秒內進行多次修改的情況。2. 只要資源被修改,無論內容是否發生實質性的變化,都會將該資源返回客戶端。 |
ETag | 1. 不存在版本問題,每次都會跟服務器進行校驗,符合則304不返回資源,不符合則重新返回資源。2. 可以更加精確的判斷資源是否被修改。3. 可以識別一秒內多次修改的情況。 | 1. 計算ETag值會對性能造成一定消耗。2. 分布式服務器存儲的情況下,需要保證計算ETag的算法一致。如果不一致,會導致資源在不同服務器上驗證不通過。 |
四、最佳實踐
從上面各類緩存的優缺點可以看出,每一種緩存都不是完美的。所以建議像下面這樣做:
- 不要緩存HTML,避免緩存后用戶無法及時獲取到更新內容。
- 使用
Cache-Control
和ETag
來控制HTML中所使用的靜態資源的緩存。一般是將Cache-Control
的max-age
設成一個比較大的值,然后用ETag
進行驗證。 - 使用簽名或者版本來區分靜態資源。這樣靜態資源會生成不同的資源訪問鏈接,不會產生修改之后無法感知的情況。
還有兩個本文沒有介紹的內容,但是不建議大家使用:
- 使用HTML的meta標簽來指定緩存行為
- 使用查詢字符串來避免緩存。因為緩存有一些已知的問題,使用查詢字符串會導致有些代理服務器不緩存資源。
五、小試牛刀,看看你掌握了沒有?
如果首次訪問localhost:1030
時,頁面中 avatar.png 響應頭信息如下:
HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Type: image/png
Last-Modified: Tue, 16 Oct 2018 11:42:28 GMT
Accept-Ranges: bytes
Date: Tue, 16 Oct 2018 15:57:21 GMT
問題1:請問當刷新該頁面后,avatar.png如何二次加載?
問題2:如果將上述信息中的Cache-Control
設置為 private,那么結果又會如何呢?
大家先回憶下上面的內容,思考一下。
好了公布答案。
問題1:會帶著If-Modified-Since
和服務端進行驗證。未改變返回304,改變返回200.
問題2:Cache-Control
設置為 private,這時候會觸發啟發式緩存,則再次刷新時,avatar.png命中強緩存,從緩存中換取。
總結
好了,文章到此結束,希望能對大家有幫助。