背景
很多時候,當開啟瀏覽器的開發者工具,檢視網路請求,對於資源大小(Size)選項,除了有具體的數字大小,還有from memory cache、from disk cache欄位之類出現。
這裡就有很多疑問,這些欄位代表著什麼意思?這些欄位又是誰來決定的?
快取位置
從字面意思理解,大概也能猜到,這些欄位代表著快取位置。
按優先順序,Size選項欄位可分為:
- from Service Worker
- from memory cache
- from disk cache
- 真正的網路請求(顯示資源的具體大小)
Service Worker
本質是作為伺服器與客戶端之間的代理伺服器,伴隨著PWA出現。Service Worker真正意義上將快取控制權交給了前端,相比於LocalStorage、SessionStorage,後兩者只是單純的介面資料快取,例如使用者資訊(一個物件)、列表資訊(一個陣列),而前者可以快取靜態資源,甚至攔截網路請求,根據網路狀況作出不同的快取策略。當然,這不是本文討論的重點。
memory cache
顧名思義,這個是將資源快取在了記憶體中。事實上,所有的網路請求都會被瀏覽器快取到記憶體中,當然,記憶體容量有限,快取不能無限存放在記憶體中,因此,註定是個短期快取。
記憶體快取的控制權在瀏覽器,前後端都不能干涉。
disk cache
與記憶體快取相對的,這個是將資源快取在硬碟中。雖然相比於記憶體,硬碟的讀取速度要慢很多,但總比沒有強。
硬碟快取的控制權在後端,通過什麼控制呢?通過HTTP響應頭控制,這是本文重點討論的。
快取策略
disk cache也叫http cahce,因為其嚴格遵守http響應頭欄位來判斷哪些資源是否要被快取,哪些資源是否已經過期。絕大多數快取都是disk cache。
disk cahce分為強制快取與對比快取。
強制快取
控制強制快取的有兩種http響應頭欄位:
Expires: Fri, 08 Feb 2019 05:37:33 GMT
欄位的值就代表了資源的過期時間,不過這個值是相對於客戶端,並且客戶端本地時間可以任意修改,因此這個欄位並不可靠。Expires欄位是Http 1.0的,Http 1.1 用Cache-Control欄位替代它:
Cache-Control: max-age=2592000
Cache-control欄位使用了絕對時間,單位為秒,即最大有效時間,在有效時間內,客戶端直接從硬碟中讀取資源。
看個例子,用Node.js搭建一個靜態資源伺服器,設定Cache-Control: max-age=2592000,每次請求都會被伺服器列印出:
const server = http.createServer((req, res) => {
console.log(`收到請求,請求地址為: ${req.url}`);
fs.readFile(path.resolve(__dirname, `./image.png`), (err, file) => {
if (err) {
res.end(err.message);
}
res.setHeader(`Cache-control`, `max-age=2592000`);
res.end(file);
});
}).listen(3000);
console.log(`localhost:3000服務已開啟!`);
複製程式碼
第一次訪問:
第二次訪問:
可以看到,第一次請求,瀏覽器根據響應頭中的Cache-Control欄位,將資源快取在硬碟中,第二次請求,瀏覽器直接從硬碟中讀取資源,並沒有傳送網路請求到伺服器。
Cache-Control欄位有以下可取值:
- max-age=xxx,最大的有效時間
- must-revalidate,如果超過了max-age的時間,必須向伺服器傳送請求,驗證資源的有效性
- no-cache,基本等價於max-age=0,由對比快取來決定是否快取資源
- no-store,真正意義上的不快取
- public,所有內容都可以被快取
- private,所有內容只有客戶端可以快取,代理伺服器不能快取。預設值
對比快取
不同於強制快取,瀏覽器直接根據響應頭Cache-Control欄位直接判斷快取資源是否有效,對比快取需要再次向伺服器確認。
Last-Modified & If-Modified-Since
伺服器通過響應頭Last-Modified告知瀏覽器,資源最後被修改的時間:
Last-Modified: Fri, 08 Feb 2019 15:20:04 GMT
當再次請求該資源時,瀏覽器需要再次向伺服器確認,資源是否過期,其中的憑證就是請求頭If-Modified-Since欄位,值為上次請求中響應頭Last-Modified欄位的值:
If-Modified-Since: Fri, 08 Feb 2019 15:20:04 GMT
伺服器會接收If-Modified-Since欄位的值與被請求資源的最後修改時間作比較
如果If-Modified-Since的值大於被請求資源的最後修改時間,則說明瀏覽器快取的資源仍然有效,伺服器會返回304狀態碼,告知瀏覽器直接取快取即可。其中伺服器返回的只有Http頭部,並不包含主體(不然就沒有快取的意義了)。
否則,就跟正常的請求一樣,伺服器返回200狀態碼,並附帶最新的資源。
看個例子,稍微修改下剛才的Node.js程式碼:
const server = http.createServer((req, res) => {
console.log(`收到請求,請求地址為: ${req.url}`);
const filename = path.resolve(__dirname, `./image.png`);
fs.stat(filename, (err, stat) => {
const lastModified = stat.mtime.toUTCString();
if (lastModified === req.headers[`if-modified-since`]) {
res.writeHead(304, `Not Modified`);
res.end();
}
else {
fs.readFile(filename, (err, file) => {
if (err) {
res.end(err.message);
}
res.setHeader(`Last-Modified`, lastModified);
res.end(file);
});
}
});
}).listen(3000);
console.log(`localhost:3000服務已開啟!`);
複製程式碼
第一次請求:
第二次請求:
比對兩次請求可以看到,除了狀態碼變成了304,資源大小也從57.8K降到了90B,這也證明響應中不包含http主體。
Etag & If-None-Match
Last-Modiflied與Expires一樣,也是有缺陷的。如果,資源的變化的時間間隔小於秒級,比如說是毫秒級的,或者說資源直接是動態生成的,那根據Last-Modified判斷,資源就是每時每刻都最新的,即被修改過!
所以,Etag & If-Node-Match 就是來解決這個問題的。
Etag欄位的值為檔案的特殊標識,一般都是hash生成的,伺服器儲存著資源的Etag值。接下來的流程都與Lst-Modified & If-Modified-Since一致,只不過,比較的值從最後修改時間變成了Etag值。
Etag的優點在於,對於動態資源或者現在流行的Restful API返回的JSON資料,這些是沒有修改時間這一說法的,但是Http標準並沒有規定Etag值如何生成,因此我們通過程式碼自己生成Etag值。當然,計算Etag值會消耗伺服器效能。
優先順序
強制快取與對比快取是可以同時存在的,並且強制快取的優先順序高於對比快取。實際應用中,也是兩者共同使用的。
看個例子,在響應頭中同時加上Cache-Control與Last-Modified:
res.setHeader(`Cache-control`, `max-age=600`);
res.setHeader(`Last-Modified`, lastModified);
複製程式碼
第一次請求:
第二次請求:
可以看到,雖然有Last-Modified欄位,但還是直接從硬碟中獲取資源。
總結
Http快取策略,其實只是前端快取的一小部分,但零亂的知識點還是非常多的。最終處理快取還是瀏覽器,各瀏覽器的處理方式可能有差異,實際應用中還是要慎重考慮。
合理運用Http快取,對前端效能優化還是非常有幫助的!