Web 快取大致可以分為:資料庫快取、伺服器端快取(代理伺服器快取、CDN 快取)、瀏覽器快取。
瀏覽器快取也包含很多內容: HTTP 快取、indexDB、cookie、localstorage 等等。這裡我們只討論 HTTP 快取相關內容。
在具體瞭解 HTTP 快取之前先來明確幾個術語:
- 快取命中率:從快取中得到資料的請求數與所有請求數的比率。理想狀態是越高越好。
- 過期內容:超過設定的有效時間,被標記為“陳舊”的內容。通常過期內容不能用於回覆客戶端的請求,必須重新向源伺服器請求新的內容或者驗證快取的內容是否仍然準備。
- 驗證:驗證快取中的過期內容是否仍然有效,驗證通過的話重新整理過期時間。
- 失效:失效就是把內容從快取中移除。當內容發生改變時就必須移除失效的內容。
瀏覽器快取主要是 HTTP 協議定義的快取機制。HTML meta 標籤,例如
1 |
<META HTTP-EQUIV="Pragma" CONTENT="no-store"> |
含義是讓瀏覽器不快取當前頁面。但是代理伺服器不解析 HTML 內容,一般應用廣泛的是用 HTTP 頭資訊控制快取。
HTTP 頭資訊控制快取
大致分為兩種:強快取和協商快取。強快取如果命中快取不需要和伺服器端發生互動,而協商快取不管是否命中都要和伺服器端發生互動,強制快取的優先順序高於協商快取。具體內容下文介紹。
匹配流程(已有快取的情況下):
強快取
可以理解為無須驗證的快取策略。對強快取來說,響應頭中有兩個欄位 Expires/Cache-Control 來表明規則。
Expires
Expires 指快取過期的時間,超過了這個時間點就代表資源過期。有一個問題是由於使用具體時間,如果時間表示出錯或者沒有轉換到正確的時區都可能造成快取生命週期出錯。並且 Expires 是 HTTP/1.0 的標準,現在更傾向於用 HTTP/1.1 中定義的 Cache-Control。兩個同時存在時也是 Cache-Control 的優先順序更高。
Cache-Control
Cache-Control 可以由多個欄位組合而成,主要有以下幾個取值:
1. max-age 指定一個時間長度,在這個時間段內快取是有效的,單位是s。例如設定 Cache-Control:max-age=31536000,也就是說快取有效期為(31536000 / 24 / 60 * 60)天,第一次訪問這個資源的時候,伺服器端也返回了 Expires 欄位,並且過期時間是一年後。
在沒有禁用快取並且沒有超過有效時間的情況下,再次訪問這個資源就命中了快取,不會向伺服器請求資源而是直接從瀏覽器快取中取。
2. s-maxage 同 max-age,覆蓋 max-age、Expires,但僅適用於共享快取,在私有快取中被忽略。
3. public 表明響應可以被任何物件(傳送請求的客戶端、代理伺服器等等)快取。
4. private 表明響應只能被單個使用者(可能是作業系統使用者、瀏覽器使用者)快取,是非共享的,不能被代理伺服器快取。
5. no-cache 強制所有快取了該響應的使用者,在使用已快取的資料前,傳送帶驗證器的請求到伺服器。不是字面意思上的不快取。
6. no-store 禁止快取,每次請求都要向伺服器重新獲取資料。
協商快取
快取的資源到期了,並不意味著資源內容發生了改變,如果和伺服器上的資源沒有差異,實際上沒有必要再次請求。客戶端和伺服器端通過某種驗證機制驗證當前請求資源是否可以使用快取。
瀏覽器第一次請求資料之後會將資料和響應頭部的快取標識儲存起來。再次請求時會帶上儲存的頭部欄位,伺服器端驗證是否可用。如果返回 304 Not Modified,代表資源沒有發生改變可以使用快取的資料,獲取新的過期時間。反之返回 200 就相當於重新請求了一遍資源並替換舊資源。
Last-modified/If-Modified-Since
Last-modified: 伺服器端資源的最後修改時間,響應頭部會帶上這個標識。第一次請求之後,瀏覽器記錄這個時間,再次請求時,請求頭部帶上 If-Modified-Since 即為之前記錄下的時間。伺服器端收到帶 If-Modified-Since 的請求後會去和資源的最後修改時間對比。若修改過就返回最新資源,狀態碼 200,若沒有修改過則返回 304。
注意:如果響應頭中有 Last-modified 而沒有 Expire 或 Cache-Control 時,瀏覽器會有自己的演算法來推算出一個時間快取該檔案多久,不同瀏覽器得出的時間不一樣,所以 Last-modified 要記得配合 Expires/Cache-Control 使用。
Etag/If-None-Match
由伺服器端上生成的一段 hash 字串,第一次請求時響應頭帶上 ETag: abcd,之後的請求中帶上 If-None-Match: abcd,伺服器檢查 ETag,返回 304 或 200。
關於 last-modified 和 Etag 區別,已經有很多人總結過了:
- 某些伺服器不能精確得到資源的最後修改時間,這樣就無法通過最後修改時間判斷資源是否更新。
- Last-modified 只能精確到秒。
- 一些資源的最後修改時間改變了,但是內容沒改變,使用 Last-modified 看不出內容沒有改變。
- Etag 的精度比 Last-modified 高,屬於強驗證,要求資源位元組級別的一致,優先順序高。如果伺服器端有提供 ETag 的話,必須先對 ETag 進行 Conditional Request。
注意:實際使用 ETag/Last-modified 要注意保持一致性,做負載均衡和反向代理的話可能會出現不一致的情況。計算 ETag 也是需要佔用資源的,如果修改不是過於頻繁,看自己的需求用 Cache-Control 是否可以滿足。
選擇 Cache-Control 的策略(摘自 Google Developers)
實際應用
回到實際應用上來,首先要明確哪些內容適合被快取哪些不適合。
考慮快取的內容:
- css樣式檔案
- js檔案
- logo、圖示
- html檔案
- 可以下載的內容
一些不應該被快取的內容:
- 業務敏感的 GET 請求
可快取的內容又分為幾種不同的情況:
不經常改變的檔案:
給 max-age 設定一個較大的值,一般設定 max-age=31536000
比如引入的一些第三方檔案、打包出來的帶有 hash 字尾 css、js 檔案。一般來說檔案內容改變了,會更新版本號、hash 值,相當於請求另一個檔案。
標準中規定 max-age 的值最大不超過一年,所以設成 max-age=31536000。至於過期內容,快取區會將一段時間沒有使用的檔案刪除掉。
有看到用對話的形式來描述這個過程,便仿照著試圖更清晰地解釋:
可能經常需要變動的檔案:
Cache-Control: no-cache / max-age=0
比如入口 index.html 檔案、檔案內容改變但名稱不變的資源。選擇 ETag 或 Last-Modified 來做驗證,在使用快取資源之前一定會去伺服器端做驗證,命中快取時會比第一種情況慢一點點,畢竟還要發請求進行通訊。
注意: 這裡只描述了最基本的思路,實際使用 HTTP 快取需要後端配合配置,具體情況具體對待,而且各方的實現並不一定完全按照標準來的,踩踩坑更健康?。
參考文章
https://jakearchibald.com/2016/caching-best-practices
https://zhuanlan.zhihu.com/p/28113197
https://stackoverflow.com/questions/12908766/what-is-cache-control-private
http://www.alloyteam.com/2016/03/discussion-on-web-caching/#prettyPhoto