前言
為什麼要寫這篇文章?
故事背景發生在 2019 年的某一天,國內某大型開源 CDN 網站伺服器奔潰之後。公司的 H5 產品,引用了他們的 CDN 資源,當天無法訪問,後來我們緊急釋出了熱修復,用阿里雲 CDN 資源替換了線上的連結地址。
釋出熱補丁之後,我們期望的結果當然是使用者重新開啟微信公眾號訪問 H5 就能正常載入最新的資源,正常訪問。
但是,最後的問題是,iOS 系統的微信公眾號能夠正常訪問,安卓卻不行,安卓載入的還是快取中的靜態資原始檔。
這裡面當然是因為微信安卓的瀏覽器核心對快取做了特殊的處理(坑),但是怎麼從根本上解決快取問題?
關於 HTTP 快取的文章網上很多,所以我這裡主要做簡單總結和問題探討。
問題:
- HTTP 快取策略是什麼樣的原理?
- 快取響應的狀態碼 200 和 304 狀態碼有什麼區別?
- 如何高效的利用快取?
- 假如想利用 HTTP 請求統計使用者訪問量,有快取不就沒用了?
先給結論:
- 快取策略分為強制快取和協商快取
- 快取位置分為 CacheStorage、記憶體快取、硬碟快取
- 200 代表從本地快取中獲取,304 代表和伺服器進行過一次通訊後從瀏覽器快取中獲取
- 儘可能將靜態資源的快取週期變長,index.html 不應該被快取,利用 GET 請求進行快取
- 利用 1*1 px 透明圖片埋點和 referer 來統計使用者訪問量,該圖片路徑不快取
一、HTTP 快取
快取是一種儲存資源副本並在下次請求時直接使用該副本的技術
瀏覽器快取一般只有 GET 請求才有效。
快取的種類有很多,其大致可歸為兩類:私有與共享快取。共享快取儲存的響應能夠被多個使用者使用。私有快取只能用於單獨使用者。
"public" 指令表示該響應可以被任何中間人(譯者注:比如中間代理、CDN等)快取,隱含的意思是,其他使用者可能也能分享這個資源的快取
private 是指資源應該被快取,但是隻能被客戶端的瀏覽器快取.
下面是和 HTTP 快取有關的 HTTP 訊息頭:
HTTP Header | 出現在 | 應用在 |
---|---|---|
Cache-Control | 請求、響應 | 強制和協商快取 |
Expires | 響應 | 強制快取 |
Last-Modified | 響應 | 協商快取 |
If-Modified-Since | 請求 | 協商快取 |
ETag | 響應 | 協商快取 |
If-None-Match | 請求 | 協商快取 |
二、快取位置
檔案的快取位置,目前普遍使用的有三種,其中 cacheStorage 是在 ServiceWorker 中應用中產生的。
快取位置 | 資源 |
---|---|
from ServiceWorker(cacheStorage) | 通過 ServiceWorker 註冊安裝 |
from memory cache | 指令碼、字型、圖片 |
from disk cache | 非指令碼:css、svg |
快取放在記憶體還是硬碟中,也會和檔案大小有關係。
三、強制快取
強制快取是通過 Expires 和 Cache Control 的 max-age 來判斷快取是否過期的策略,此時不會向伺服器發起請求。
如果快取沒有過期,將會直接從瀏覽器快取中獲取資源。
Expires 的值通常是一個絕對時間,存在的問題當是客戶端和伺服器時間不一致或者被修改的情況下就會失效。而 max-age 的值是相對時間,根據瀏覽器快取根據伺服器返回的 Date 和 Max-Age 判斷快取是否過期。
打個比方:max-age 相當於說“保質期六個月”,而 Expires 是說“在此日期之前”飲用。
max-age 和 Expires 設定的快取過期時間最多為一年(365天),如果多於這個值則瀏覽器有可能會忽略
當 Expires 和 Max-Age 同時存在時,會忽略 Expires 指令。
作為請求首部時,cache-directive 的可選值
欄位名稱 | 說明 |
---|---|
no-cache | 告知(代理)伺服器不直接使用快取,要求向原伺服器發起請求 |
no-store | 所有內容都不會被儲存到快取或 Internet 臨時檔案中,直接下載檔案 |
max-age=delta-seconds | 告知伺服器客戶端希望接收一個存在時間(Age)不大於 delta-seconds 秒的資源, 為 0 表示直接進行協商快取 |
max-stale[=delta-seconds] | 告知(代理)伺服器端願意接收一個超過快取時間的資源,若定義delta-seconds則為delta-seconds秒,若沒有則為任意超出時間 |
min-fresh=delta-seconds | 所有內容都不會被儲存到快取或 Internet臨時檔案中 |
no-transform | 告知(代理)伺服器客戶端希望獲取實體資料沒有被轉換(比如壓縮)過的資源 |
only-if-cached | 告知(代理)伺服器客戶端希望獲取快取的內容(若有), 而不用向原伺服器發去請求 |
cache-extension | 自定義擴充套件值,若伺服器不識別該值將被忽略掉 |
Cache-Control 在響應中的欄位:
欄位名稱 | 說明 |
---|---|
public | 指示響應可以被任何快取所快取,即使通常它只是非可快取或可快取到一個非共享快取內 |
private | 指示響應資訊的全部或者部分用於單個使用者,而不能用一個共享快取來快取 |
no-cache | 可以快取,但是隻有在跟 WEB 伺服器驗證了其有效後,才能返回給客戶端(直接進行協商快取) |
no-store | 所有內容不會被儲存到快取或 Internet 臨時檔案中,指令的目的是防止無心釋出或是保留了敏感資訊(例如,備份) |
no-transform | 告知客戶端快取檔案時不得對實體資料做任何改變。它對__轉換某個實體體的媒體型別__很有用,例如,一個非透明的代理把影像轉換格式,以節省快取空間,或是減少慢速連結中的通訊量 |
only-if-cached | 在某些情況下,如網路連線非常差時,客戶端可能需要一個快取,只返回目前已儲存的那些響應,而不是重新載入,或與源伺服器重新驗證。要做到這一點,客戶端可以在一個請求中包含該指令。 |
must-revalidate | 當前資源一定是向原伺服器發去驗證請求的,若請求失敗會返回__504__(而非代理伺服器上的快取) |
proxy-revalidate | 與 must-revalidate類似,但是僅能應用於共享快取(如代理) |
max-age=delta-seconds | 告知客戶端該資源在delta-seconds秒內是新鮮的,除非還包含 max-stale 指令,否則客戶端不期望接收一個陳舊的響應。 |
s-maxage=delta-seconds | 同 max-age,但僅應用於共享快取(如代理) |
cache-extension | 自定義擴充套件值,若伺服器不識別該值將被忽略 |
四、協商快取
Last-Modified 和 If-Modified-Since 是用最後修改日期時間判斷一個快取的資源是否有效。
- 伺服器第一次請求會傳送 Last-Modified 響應頭
- 瀏覽器下次協商請求時,會將 If-Modified-Since 頭加上 快取響應中的 Last-Modified 值合在一起傳送給伺服器,
- 伺服器判斷沒過期,就會返回 304 狀態碼,如果過期會直接返回資源
- 如果狀態碼是 304, 瀏覽器就從快取中獲取資源
Last-Modified 存在的問題可能是,將打包的前端資原始檔夾進行覆蓋式部署時,部分檔案內容並沒有發生變化,但是修改時間卻被更新了,此時瀏覽器會下載資源,造成了不必要的時延和頻寬消耗。
Etag 和 If-None-Match 則是用內容摘要作為判定的依據。內容摘要是指根據一個資源的內容產生一串比較短的數字,當內容變化時,產生的數字串也會改變。內容摘要的演算法有很多種,較常見的是SHA-1雜湊演算法、CRC32等。
Etag 的執行流程和 Last-Modified 一樣:
- 第一次請求時,伺服器返回檔案的 etag
- 後面的請求,瀏覽器將 If-None-Match 頭帶上快取響應中的 etag 值合在一起,傳送給伺服器
- 伺服器判斷沒過期,就會返回 304 狀態碼,如果過期就會直接返回資源。
- 如果狀態碼是 304, 瀏覽器就從快取中獲取資源
Etag 的優先順序會比 Last-Modified 更高, Etag 就是為了解決 Last-Modified 檔案更新時間變化,但檔案內容沒變的問題,另外 Etag 的缺點在於會佔用比 Last-Modified 更高的伺服器 CPU 消耗。
五、200 和 304
由以上的知識,我們瞭解到:
強制快取返回的 HTTP 響應狀態碼是 200,而協商快取返回的響應狀態碼是 304。
大多數情況是這樣沒錯,但是在部分瀏覽器中(比如谷歌),協商快取也是會返回 200 的,這是瀏覽器的演算法使然,瀏覽器判斷,越長時間沒有更新的檔案,會直接從瀏覽器快取中獲取資源,此時狀態碼是 200.
下面是網上常見強制快取和協商快取的流程圖,我也畫了一個:
六、快取更新
這個問題主要在產品部署釋出迭代版本的時候會遇到。
如果伺服器沒有配置 Last-Modified 和 etag, 當強制快取還沒有失效的時候,如何更新檔案版本呢?
目前普遍的做法有兩種,不快取 index.html,在 js,css,字型,圖片的連結地址後拼接版本引數:比如
或者將資原始檔的內容 HASH 短碼新增在檔名中:
七、談談微信瀏覽器的坑
微信安卓版內建瀏覽器符合一般瀏覽器的快取策略。但是 iOS 內建瀏覽器會將 index.html 檔案強制快取,儘管你在 index.html 檔案的 head 標籤上新增了如下程式碼:
<meta http-equiv="Cache-Control" content="no-cache, no-store" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="expires" content="0" />
複製程式碼
上面 Pragma 是 HTTP1.0 的訊息頭,基本沒用,僅有 IE 才能識別這段 meta 標籤含義。 解決方案就是從伺服器層面對 index.html 檔案進行快取控制,比如 Nginx:
location / {
root /var/www/;
index index.html index.htm;
try_files $uri $uri/ /index.html;
#### kill cache
add_header Last-Modified $date_gmt;
add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
if_modified_since off;
expires off;
etag off;
}
location ~* ^.+\.(jpg|jpeg|gif|png|ico|css|js)$ {
root /mnt/dat1/test/tes-app;
access_log off;
expires 30d;
}
複製程式碼
大公司一般採用 index.html 和 js、css、字型、圖片資源利用 CDN 分開部署,不在同一臺伺服器上,所以只需要對 index.html 設定不快取即可。
順便提一下,如果是字型檔案部署在 CDN 上,會存在跨域問題(CSS、JS、圖片不會受到同源政策的限制),開啟 CORS 即可:
access-control-allow-origin: *
複製程式碼
總結
瀏覽器快取是前端效能優化中關鍵的一步,利用 GET 請求快取檔案。
瀏覽器快取分為強制快取和協商快取,強制快取中 Cache-Control 比 Expires 的優先順序更高。協商快取中,If-None-Match 比 If-Modified-Since 的優先順序更高。
強制快取返回狀態碼 200,協商快取狀態 304。
前端打包應該利用打包工具對檔名新增 hash 短碼,如需校驗身份資訊應該利用協商快取和私有快取。
參考連結:
- 知乎:大公司裡怎樣開發和部署前端程式碼?
- 掘金:實踐這一次,徹底搞懂瀏覽器快取機制
- MDN:HTTP 快取
- Caching Tutorial for Web Authors and Webmasters
@Starbucks 2019/04/13