瀏覽器緩存策略

今天看奇舞團推了篇文章講緩存策略的,講的挺不錯,記錄一下。 原文地址就在下面。

總結:

  1. 緩存分為強緩存和協商緩存。其中強緩存包括ExpiresCache-Control,主要是在過期策略生效時應用的緩存。弱緩存包括Last-ModifiedETag,是在協商策略后應用的緩存。強弱緩存之間的主要區別在于獲取資源時是否會發送請求
  2. Cache-Control中的max-age指令用于指定緩存過期的相對時間,優先級高于ExpiresCache-Control指定為no-cache時,由于no-cache相當于max-age:0,must-revalidate,所以都存在時優先級也是高于Expires
  3. no-cache并不是指不緩存文件,no-store才是指不緩存文件。no-cache僅僅是表明跳過強緩存,強制進入協商策略。
  4. 如果ExpiresCache-Control: max-age,或 Cache-Control:s-maxage都沒有在響應頭中出現,并且設置了Last-Modified時,那么瀏覽器默認會采用一個啟發式的算法,即啟發式緩存。通常會取響應頭的Date_value - Last-Modified_value值的10%作為緩存時間

原文鏈接,以下是原文


前言

眾所周知,在Web開發中,緩存很重要、很有用。但同時其也很復雜。

本文將從以下5個方面全面地介紹下緩存相關的內容。

  1. 緩存的判斷策略
  2. 必知必會的緩存基礎
  3. 各類緩存的優缺點
  4. 緩存的最佳實踐
  5. 小試牛刀,看看你掌握了沒有?

一、緩存的判斷策略

瀏覽器對于所請求資源的緩存處理有一套完整的機制,主要包含以下三個策略:存儲策略、過期策略、協商策略

其中,存儲策略發生在收到請求響應后,用于決定是否緩存相應資源;過期策略發生在請求前,用于判斷緩存是否過期;協商策略發生在請求中,用于判斷緩存資源是否更新。

瀏覽器在應用緩存策略時,具體的判斷流程如下:

瀏覽器緩存判斷策略

上圖中的緩存判斷流程是瀏覽器在應用緩存時完整的判斷流程。但是在瀏覽器中訪問資源的方式不同也會導致判斷流程的不同。判斷流程會根據不同方式跳過一些流程。

瀏覽器下訪問資源的方式主要有以下7種:

  1. (新標簽)地址欄回車
  2. 鏈接跳轉
  3. 前進、后退
  4. 從收藏欄打開鏈接
  5. (window.open)新開窗口
  6. 刷新(Command + R / F5)
  7. 強制刷新(Command + Shift + R / Ctrl + F5)

使用這7種方式訪問資源時,應用緩存的策略會有一些不同。

需要注意的是,除此之外,還有一種特殊情況。即在當前地址欄,不改變內容,直接回車,等同于刷新當前頁。但是在當前頁點擊跳轉到自身,和鏈接跳轉一致,并不會等同于刷新。

如下圖所示。通過上述8種方式訪問資源,會從不同的緩存應用判斷步驟開始。此處不做驗證,相信大家看了后面的內容,能夠自行驗證的。


不同訪問方式下的瀏覽器資源判斷

本文配有測試腳本,代碼在github上。下文會按照測試腳本進行述說,使用說明見下載鏈接。驗證上述內容,可以執行node cache-ETag+max-age.js,會同時開啟ETagmax-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之所以是半對,是因為有可能會觸發啟發式緩存,也會緩存文件。具體見下文。

緩存又分為強緩存和弱緩存(又稱為協商緩存)。其中強緩存包括ExpiresCache-Control,主要是在過期策略生效時應用的緩存。弱緩存包括Last-ModifiedETag,是在協商策略后應用的緩存。強弱緩存之間的主要區別在于獲取資源時是否會發送請求

2.1 Expires

如上所述,Expires指定緩存的過期時間,為絕對時間,即某一時刻。參考本地時間進行比對,在指定時刻后過期。RFC 2616建議最大值不要超過1年

Expire頭字段是響應頭字段,格式如下:Expires: Sat Oct 20 2018 00:00:00 GMT+0800 (CST)

可以嘗試以下步驟進行驗證:

  1. 執行node cache-Expires.js,該腳本會給請求的資源設定Expires,值為:”2018-10-20 00:00:00”。

  2. 訪問地址http://localhost:1030/,開啟Network Tab,查看avatar.jpg圖片,Expires值如下所示。

  3. 再次刷新會看到該資源已經被緩存,size欄顯示為(from memory cache)。此時修改本地時間,將時間修改為“2018-10-15 00:00:00”,再刷新,會發現緩存仍然有效。

    Expires緩存生效

  4. 如果將本地時間修改為“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-ageExpires頭字段 ? ? ?
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時,指令可以不區分大小寫。不過建議統一使用小寫。驗證如下:

  1. 執行node cache-directive-case-insensitive.js,會服務端會將max-age寫成大寫,如下Cache-Control: MAX-AGE=86400
  2. 再次請求瀏覽器會發現緩存同樣會生效。

2.3.2 在請求頭中的max-age

max-age在請求頭中的主要應用為max-age=0表示不使用緩存。Chrome和Firefox瀏覽器下的刷新操作(Command+ R / F5)均是在請求頭上添加了max-age=0?指令,表示不使用強緩存,但允許協商緩存(在介紹了協商緩存的Last-ModifiedETag之后,可以自行驗證下這一點)。

刷新時Cache-Controlmax-age=0驗證如下:

  1. 單獨訪問圖片資源http://localhost:1030/avatar.jpg,開啟Network

  2. 刷新,可在響應頭中看到上述內容。如下圖所示。(Firefox下相同,不單獨驗證,主要最開始提到的主資源和派生資源在兩個瀏覽器中表現形式的不同)。

    Chrome下刷新時,請求中的max-age值

    此外,經驗證,Chrome和Firefox均對max-age>0的情況支持不好。

  3. 在Chrome下,通過Modify Headers插件(Chrome和Firefox下均有類似插件)給請求添加max-age=7200

  4. 執行node cache-max-age.js,訪問http://localhost:1030,先強刷保證資源更新。

  5. 打開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。驗證如下:

  1. 執行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. 刷新,然后再把本地時間改成當前時間延后2小時(不超過20號),會發現緩存生效。(以下兩步不再附截圖,與上述示例類似)。
  3. 如果將時間改為兩天后(假設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-cacheCache-Control:no-cache并且不會帶上協商策略中所涉及的信息(下面介紹的If-Modified-Since/If-None-Match。這是不會使用任何緩存,重新獲取資源。如下圖所示。

強刷瀏覽器自動設置no-cache

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%作為緩存時間。驗證如下:

  1. 執行node cache-Last-Modified.js,服務器會獲取資源的最后修改時間,設置為Last-Modified的值。訪問localhost:1030,查看avatar.jpg,如下圖所示:
    Last-Modified設定
  2. 刷新瀏覽器,會發現圖片會從緩存獲取。
  3. 通過啟發式緩存的公司可以計算出緩存的時間,修改本地時間超過緩存時間后,再刷新,會發現緩存失效。

2.4.1 If-Modified-Since

返回的資源帶有Last-Modified標識時,再次請求該資源,瀏覽器會自動帶上If-Modified-Since,值為返回的Last-Modified值。請求到達服務器后,服務器進行判斷,如果從上次更新后沒有再更新,則返回304。如果更新了則重新返回。驗證如下:

  1. 執行node cache-Last-Modified.js,服務器會獲取資源的最后修改時間,設置為Last-Modified的值。如下圖所示,并且注意看一下資源的大小。
    Last-Modified設定
請求資源大小
  1. 刷新頁面,再次查看NetWork。會發現請求頭中帶上了If-Modified-Since。如果服務器判斷資源未改變,則返回304,此外由于服務器返回304,資源會從緩存獲取,所以資源大小也減少了,如下所示。

    304 請求資源大小

  2. 修改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)狀態碼的響應。主要有如下兩種場景。

  1. 用于不安全的請求中從而是請求具備條件性(如POST或者其他不安全的方法),如請求更新wiki文檔,文檔未修改時才執行更新。
  2. 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。當ETagLast-ModifiedETag優先級更高,但不會忽略Last-Modified,需要服務端實現。驗證如下,其中服務端判斷優先級:

  1. 執行node cache-ETag+Last-Modified.js。服務端會在資源的響應頭中,同時設置ETagLast-Modified。如下圖:

    同時設置ETag和Last-Modified

  2. 刷新瀏覽器,會發現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的算法一致。如果不一致,會導致資源在不同服務器上驗證不通過。

四、最佳實踐

從上面各類緩存的優缺點可以看出,每一種緩存都不是完美的。所以建議像下面這樣做

  1. 不要緩存HTML,避免緩存后用戶無法及時獲取到更新內容。
  2. 使用Cache-ControlETag來控制HTML中所使用的靜態資源的緩存。一般是將Cache-Controlmax-age設成一個比較大的值,然后用ETag進行驗證。
  3. 使用簽名或者版本來區分靜態資源。這樣靜態資源會生成不同的資源訪問鏈接,不會產生修改之后無法感知的情況。

還有兩個本文沒有介紹的內容,但是不建議大家使用:

  1. 使用HTML的meta標簽來指定緩存行為
  2. 使用查詢字符串來避免緩存。因為緩存有一些已知的問題,使用查詢字符串會導致有些代理服務器不緩存資源。

五、小試牛刀,看看你掌握了沒有?

如果首次訪問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命中強緩存,從緩存中換取。

總結

好了,文章到此結束,希望能對大家有幫助。

參考鏈接

  1. MDN | Cache-Control
  2. 徹底弄懂 Http 緩存機制 - 基于緩存策略三要素分解法
  3. 由memoryCache和diskCache產生的瀏覽器緩存機制的思考
  4. A Web Developer’s Guide to Browser Caching
  5. 瀏覽器緩存機制剖析
  6. HTTP 緩存
  7. Are Your Cache-Control Directives Doing What They Are Supposed to Do?
  8. Hypertext Transfer Protocol
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,677評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,643評論 2 380

推薦閱讀更多精彩內容