瀏覽器緩存策略之掃盲篇

前言

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

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

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

1、緩存的判斷策略

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

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

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

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

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

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

使用這 7 種方式訪問資源時,應用緩存的策略會有一些不一樣。以下圖所示。經過上述 7 種方式訪問資源,會從不一樣的緩存應用判斷步驟開始。此處不作驗證,相信你們看了後面的內容,可以自行驗證的。 不一樣訪問方式下的瀏覽器資源判斷瀏覽器

須要注意的是,Chrome 中在當前地址欄,不改變內容,直接回車,等同於刷新當前頁,而在 Firefox 下與其餘在地址欄回車同樣。這一點比較特殊,須要適當區分下。緩存

本文配有測試腳本,代碼在github上。下文會按照測試腳本進行述說,使用說明見下載連接。驗證上述內容,能夠執行node cache-ETag+max-age.js,會同時開啓ETagmax-age,而後觸發相應的動做,經過 Network 面板和 node 日誌便可驗證,此處篇幅有限先不贅述。安全

此外,這裏提一個概念,webkit 資源分爲主資源和派生資源。主資源是地址欄輸入的 URL 請求返回的資源,派生資源是主資源中所引用的 JS、CSS、圖片等資源。

在 Chrome 下刷新時,只有主資源的緩存應用方式如上圖所示,派生資源的緩存應用方式與新標籤打開相似,會判斷緩存是否過時。強緩存生效時的區別在於新標籤打開爲from disk cache,而當前頁刷新派生資源是from memory cache

而在 Firefox 下,當前頁面刷新,全部資源都會如上圖所示。下文也會利用 Chrome 的這一特色在當前頁刷新,派生資源會使用緩存進行測試。否則每次都須要打開新標籤較爲繁瑣。

2、必知必會的緩存基礎

HTTP 中與緩存有關的字段主要有如下 10 個,以下表所示。爲明確表示其功能及用法,下表中分別區分了存儲策略、過時策略、協商策略、請求頭、響應頭。

緩存相關頭字段表格
緩存相關頭字段表格

注:乄表示半對,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 值以下所示。 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能夠設置多個緩存指令,以逗號,分隔。

Cache-Control 指令表
Cache-Control 指令表

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 的狀況支持很差。

  1. 在 Chrome 下,經過 Modify Headers插件(Chrome 和 Firefox 下均有相似插件)給請求添加 max-age=7200
  2. 執行 node cache-max-age.js,訪問 http://localhost:1030,先強刷保證資源更新。
  3. 打開 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
強刷瀏覽器自動設置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設定 請求資源大小
  2. 刷新頁面,再次查看 NetWork。會發現請求頭中帶上了 If-Modified-Since。若是服務器判斷資源未改變,則返回 304,此外因爲服務器返回 304,資源會從緩存獲取,因此資源大小也減小了,以下所示。 304 資源未修改 304 請求資源大小
  3. 修改 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生效。以下: ETag生效,優先級更高

3、緩存的優缺點

好了,經過長長的第二部分,咱們簡單介紹了一下 HTTP Cache 的基礎知識。下面我再彙總一下各種緩存之間的優缺點吧。以下表所示:

緩存優缺點表
緩存優缺點表

4、最佳實踐

從上面各種緩存的優缺點能夠看出,每一種緩存都不是完美的。因此建議像下面這樣作

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

還有兩個本文沒有介紹的內容,可是不建議你們使用:

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

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

看了這麼多內容,是時候來看當作果了。那麼一塊兒看下下面的問題吧。

若是首次訪問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,那麼結果又會如何呢?

你們先回憶下上面的內容,思考一下。

試題來源:完全弄懂 Http 緩存機制 - 基於緩存策略三要素分解法。在此致謝。

好了公佈答案。

問題 1:會帶着If-Modified-Since和服務端進行驗證。未改變返回 304,改變返回 200。

問題 2:Cache-Control設置爲 private,這時候會觸發啓發式緩存,則再次刷新時,avatar.png 命中強緩存,從緩存中換取。

總結

好了,文章到此結束,但願能對你們有幫助。

致謝

感謝《深刻淺出 Vue.js》做者劉博文對本文提出的寶貴建議。

參考連接

  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

相關文章
相關標籤/搜索