背景
很多時候,當打開瀏覽器的開發(fā)者工具,查看網(wǎng)絡請求,對于資源大小(Size)選項,除了有具體的數(shù)字大小,還有from memory cache、from disk cache字段之類出現(xiàn)。
這里就有很多疑問,這些字段代表著什么意思?這些字段又是誰來決定的?
緩存位置
從字面意思理解,大概也能猜到,這些字段代表著緩存位置。
按優(yōu)先級,Size選項字段可分為:
- from Service Worker
- from memory cache
- from disk cache
- 真正的網(wǎng)絡請求(顯示資源的具體大小)
Service Worker
本質是作為服務器與客戶端之間的代理服務器,伴隨著PWA出現(xiàn)。Service Worker真正意義上將緩存控制權交給了前端,相比于LocalStorage、SessionStorage,后兩者只是單純的接口數(shù)據(jù)緩存,例如用戶信息(一個對象)、列表信息(一個數(shù)組),而前者可以緩存靜態(tài)資源,甚至攔截網(wǎng)絡請求,根據(jù)網(wǎng)絡狀況作出不同的緩存策略。當然,這不是本文討論的重點。
memory cache
顧名思義,這個是將資源緩存在了內(nèi)存中。事實上,所有的網(wǎng)絡請求都會被瀏覽器緩存到內(nèi)存中,當然,內(nèi)存容量有限,緩存不能無限存放在內(nèi)存中,因此,注定是個短期緩存。
內(nèi)存緩存的控制權在瀏覽器,前后端都不能干涉。
disk cache
與內(nèi)存緩存相對的,這個是將資源緩存在硬盤中。雖然相比于內(nèi)存,硬盤的讀取速度要慢很多,但總比沒有強。
硬盤緩存的控制權在后端,通過什么控制呢?通過HTTP響應頭控制,這是本文重點討論的。
緩存策略
disk cache也叫http cahce,因為其嚴格遵守http響應頭字段來判斷哪些資源是否要被緩存,哪些資源是否已經(jīng)過期。絕大多數(shù)緩存都是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字段使用了絕對時間,單位為秒,即最大有效時間,在有效時間內(nèi),客戶端直接從硬盤中讀取資源。
看個例子,用Node.js搭建一個靜態(tài)資源服務器,設置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服務已開啟!');
第一次訪問:
第二次訪問:
可以看到,第一次請求,瀏覽器根據(jù)響應頭中的Cache-Control字段,將資源緩存在硬盤中,第二次請求,瀏覽器直接從硬盤中讀取資源,并沒有發(fā)送網(wǎng)絡請求到服務器。
Cache-Control字段有以下可取值:
- max-age=xxx,最大的有效時間
- must-revalidate,如果超過了max-age的時間,必須向服務器發(fā)送請求,驗證資源的有效性
- no-cache,基本等價于max-age=0,由對比緩存來決定是否緩存資源
- no-store,真正意義上的不緩存
- public,所有內(nèi)容都可以被緩存
- private,所有內(nèi)容只有客戶端可以緩存,代理服務器不能緩存。默認值
對比緩存
不同于強制緩存,瀏覽器直接根據(jù)響應頭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狀態(tài)碼,告知瀏覽器直接取緩存即可。其中服務器返回的只有Http頭部,并不包含主體(不然就沒有緩存的意義了)。
否則,就跟正常的請求一樣,服務器返回200狀態(tài)碼,并附帶最新的資源。
看個例子,稍微修改下剛才的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服務已開啟!');
第一次請求:
第二次請求:
比對兩次請求可以看到,除了狀態(tài)碼變成了304,資源大小也從57.8K降到了90B,這也證明響應中不包含http主體。
Etag & If-None-Match
Last-Modiflied與Expires一樣,也是有缺陷的。如果,資源的變化的時間間隔小于秒級,比如說是毫秒級的,或者說資源直接是動態(tài)生成的,那根據(jù)Last-Modified判斷,資源就是每時每刻都最新的,即被修改過!
所以,Etag & If-Node-Match 就是來解決這個問題的。
Etag字段的值為文件的特殊標識,一般都是hash生成的,服務器存儲著資源的Etag值。接下來的流程都與Lst-Modified & If-Modified-Since一致,只不過,比較的值從最后修改時間變成了Etag值。
Etag的優(yōu)點在于,對于動態(tài)資源或者現(xiàn)在流行的Restful API返回的JSON數(shù)據(jù),這些是沒有修改時間這一說法的,但是Http標準并沒有規(guī)定Etag值如何生成,因此我們通過代碼自己生成Etag值。當然,計算Etag值會消耗服務器性能。
優(yōu)先級
強制緩存與對比緩存是可以同時存在的,并且強制緩存的優(yōu)先級高于對比緩存。實際應用中,也是兩者共同使用的。
看個例子,在響應頭中同時加上Cache-Control與Last-Modified:
res.setHeader('Cache-control', 'max-age=600');
res.setHeader('Last-Modified', lastModified);
第一次請求:
第二次請求:
可以看到,雖然有Last-Modified字段,但還是直接從硬盤中獲取資源。
總結
Http緩存策略,其實只是前端緩存的一小部分,但零亂的知識點還是非常多的。最終處理緩存還是瀏覽器,各瀏覽器的處理方式可能有差異,實際應用中還是要慎重考慮。
合理運用Http緩存,對前端性能優(yōu)化還是非常有幫助的!