面試常客:聊聊 HTTP 緩存的一切
速度、速度,還是速度,一個(gè)網(wǎng)站要想體驗(yàn)好,就必須在第一時(shí)間以最快的速度顯示出來。mysql查詢慢,就加一層 redis 做緩存,網(wǎng)站資源加載慢,怎么做,使用 HTTP緩存。
HTTP緩存自 HTTP/1.0 就開始有,為的是減少服務(wù)器壓力,加快網(wǎng)頁響應(yīng)速度。
緩存操作的目標(biāo)
HTTP 緩存只能存儲(chǔ) GET 請(qǐng)求的響應(yīng),而對(duì)其他類型的請(qǐng)求無能為力。
緩存發(fā)展史
HTTP/1.0 提出緩存概念,即強(qiáng)緩存 Expires 和協(xié)商緩存 Last-Modified。后 HTTP/1.1 又有了更好的方案,即強(qiáng)緩存 Cache-Control 和協(xié)商緩存 ETag。
為什么 Expires 和 Last-Modified 不適用呢?
Expires 即過期時(shí)間,但問題是這個(gè)時(shí)間點(diǎn)是服務(wù)器的時(shí)間,如果客戶端的時(shí)間和服務(wù)器時(shí)間有差,就不準(zhǔn)確。所以用 Cache-Control 來代替,它表示過期時(shí)長,這就沒歧義了。
Last-Modified 即最后修改時(shí)間,而它能感知的單位時(shí)間是秒,也就是說如果在1秒內(nèi)改變多次,內(nèi)容文件雖然改變了,但展示還是之前的,存在不準(zhǔn)確的場景,所以就有了 ETag,通過內(nèi)容給資源打標(biāo)識(shí)來判斷資源是否變化。
以下表格利于對(duì)比理解:
版本 強(qiáng)緩存 協(xié)商緩存 HTTP/1.0 Expires Last-Modified HTTP/1.1 Cache-Control ETag。
兩大緩存類型對(duì)比
前文已介紹不同版本下的緩存類型。當(dāng)時(shí)提了有一句強(qiáng)緩存和協(xié)商緩存,但沒具體介紹?,F(xiàn)在來講講這兩種緩存類型。
強(qiáng)緩存
Cache-Control
- HTTP/1.1。
- 通過過期時(shí)長控制緩存,對(duì)應(yīng)的字段有很多,例如 max-age 例如 Cache-Control: max-age=3600,表示緩存時(shí)間為3600秒,過期失效。
- 緩存請(qǐng)求指令: Cache-Control: max-age= Cache-Control: max-stale[=] Cache-Control: min-fresh= Cache-control: no-cache Cache-control: no-store Cache-control: no-transform Cache-control: only-if-cached。
- 緩存響應(yīng)指令: Cache-control: must-revalidate Cache-control: no-cache Cache-control: no-store Cache-control: no-transform Cache-control: public Cache-control: private Cache-control: proxy-revalidate Cache-Control: max-age= Cache-control: s-maxage=。
- 其中關(guān)鍵點(diǎn): Cache-control: no-cache 跳過當(dāng)前的強(qiáng)緩存,發(fā)送 HTTP 請(qǐng)求(如有協(xié)商緩存標(biāo)識(shí)即直接進(jìn)入?yún)f(xié)商緩存階段)no-cache 的含義和 max-age=0 一樣 ,即跳過強(qiáng)緩存,強(qiáng)制刷新 Cache-control: no-store 不使用緩存(包括協(xié)商緩存) Cache-Control: public, max-age=31536000 一般用于緩存靜態(tài)資源public:響應(yīng)可以被中間代理、CDN 等緩存private:專用于個(gè)人的緩存,中間代理、CDN等能換緩存此響應(yīng)max-age:單位是秒。
- 更多指令參考指令大全。
Expires
- HTTP/1.0。
- 語法: Expires: 。
- 即過期時(shí)間,存在于服務(wù)器返回的響應(yīng)頭里 Expires: Mon, 11 Apr 2022 06:57:18 GMT表示資源在2022年4月11號(hào)6點(diǎn)57分過期,過期了就會(huì)往服務(wù)端發(fā)請(qǐng)求。
- 如果在Cache-Control響應(yīng)頭設(shè)置了 "max-age" 或者 "s-max-age" 指令,那么 Expires 頭會(huì)被忽略。
- 缺點(diǎn):服務(wù)器時(shí)間與瀏覽器時(shí)間可能不一致。
- 更多指令參考指令大全。
Cache-Control VS Expires
- Cache-Control 較之 Expires 更為精準(zhǔn)。
- 同時(shí)存在時(shí),Cache-Control 優(yōu)先級(jí)大于 Expires。
- Expires 是 HTTP/1.0 提出,其瀏覽器兼容性更好,Cache-Control 是 HTTP/1.1 提出,可同時(shí)存在,當(dāng)有不支持 Cache-Control 的瀏覽器時(shí)會(huì)以 Expires 為準(zhǔn)。
協(xié)商緩存
協(xié)商緩存需要配合強(qiáng)緩存使用,使用協(xié)商緩存的前提是設(shè)置強(qiáng)緩存設(shè)置 Cache-Control: no-cache或者 pragma: no-cache或者 max-age=0 告訴瀏覽器不走強(qiáng)緩存。
pragma 是 HTTP/1.0 中禁止網(wǎng)頁緩存的字段,其取值為 no-cache 和 Cache-Control 的 no-cache 效果一樣。
ETag/If-None-Match
- HTTP/1.1。
- 即生成文件唯一標(biāo)識(shí)來判斷是否過期。只要內(nèi)容改變,這個(gè)值就會(huì)變。
- 與 If-None-Match 配合,ETag是請(qǐng)求服務(wù)器后返回給每個(gè)資源文件的唯一標(biāo)識(shí),客戶端會(huì)將此標(biāo)識(shí)存在客戶端(即瀏覽器)中,下次請(qǐng)求時(shí)會(huì)在請(qǐng)求頭的 If-Nono-Match 中將其值帶上,服務(wù)器判斷 If-None-Match 是否與自身服務(wù)器上的 ETag 一致,如果一致則返回 304,重定向跳轉(zhuǎn)使用本地緩存;不一致,則返回200,將最新資源返回給客戶端,并帶上 ETag。
- 更多指令參考指令大全。
Last-Modified/If-Modified-Since
- HTTP/1.0。
- 最后修改時(shí)間,即通過最后修改時(shí)間來判斷是否過期。在瀏覽器第一次給服務(wù)器發(fā)送請(qǐng)求后,服務(wù)器會(huì)在響應(yīng)頭上加上這個(gè)字段。
- 與 If-Modified-Since 配合,客戶端訪問服務(wù)器資源時(shí),服務(wù)器端會(huì)將 Last-Modified 放入響應(yīng)頭中,即這個(gè)資源在服務(wù)器上的最后修改時(shí)間,客戶端緩存這個(gè)值,等下次請(qǐng)求這個(gè)資源時(shí),瀏覽器會(huì)檢測(cè)到請(qǐng)求頭中的 Last-Modified,于是乎添加 If-Modified-Since,如果 If-Modified-Since 的值與服務(wù)器中這個(gè)資源的最后修改時(shí)間一致,則返回 304,重定向跳轉(zhuǎn)使用本地緩存;不一致,則返回200,將最新資源返回給客戶端,并帶上 Last-Modified。
- 缺點(diǎn): 文件雖然被修改,但最后的內(nèi)容沒有變化,這樣文件修改時(shí)間還是會(huì)更新有些文件修改頻率在秒以內(nèi),這樣以秒粒度來記錄就不適用了有些服務(wù)器無法精準(zhǔn)獲取文件的最后修改時(shí)間。
- 更多指令參考指令大全。
ETag VS Last-Modified
- 精確度 ETag > Last-Modified。ETag 是通過內(nèi)容給資源打標(biāo)識(shí)來判斷資源是否變化,而 Last-Modified不一樣,在某些場景下準(zhǔn)確度會(huì)失效。例如編輯文件,但是文件內(nèi)容未變,緩存會(huì)失效;或者在1秒內(nèi)改變多次,Last-Modified能感知的單位時(shí)間是秒。
- 性能 Last-Modified > ETag。Last-Modified 僅僅記錄一個(gè)時(shí)間點(diǎn),而 ETag需要根據(jù)文件的具體內(nèi)容生成哈希值。
- 如果兩個(gè)都支持的話,服務(wù)器會(huì)優(yōu)先選擇ETag。
協(xié)商緩存的條件請(qǐng)求
前文說到協(xié)商緩存是在請(qǐng)求頭添加 If-None-Match 或 If-Modified-Since,這些請(qǐng)求頭是什么,添加有什么用?
強(qiáng)緩存是通過具體時(shí)間到期或過期時(shí)長來控制緩存,這就有個(gè)問題了,如果其中的一些文件修改了,因?yàn)閺?qiáng)緩存,瀏覽器展示的還是原來的數(shù)據(jù),所以對(duì)那種常變化的數(shù)據(jù)不能使用強(qiáng)緩存做緩存策略,于是乎,就有了協(xié)商緩存,通過文件變化告訴瀏覽器緩存失效,使用前需去服務(wù)器驗(yàn)證是否是最新版?
這樣,瀏覽器就要連續(xù)發(fā)送兩個(gè)請(qǐng)求來驗(yàn)證:
- 先是 HEAD 請(qǐng)求,獲取資源的修改時(shí)間、hash值等元信息,然后與緩存數(shù)據(jù)比較,如果沒有改動(dòng)就使用緩存。
- 否則就再發(fā)一個(gè) GET 請(qǐng)求,獲取最新的版本。
但這樣的兩個(gè)請(qǐng)求的網(wǎng)絡(luò)成本太高,所以 HTTP 協(xié)議就定義了一系列 If 開頭的條件請(qǐng)求字段,專門用來檢查驗(yàn)證資源是否過期,把兩個(gè)請(qǐng)求合并在一個(gè)請(qǐng)求中做。而且驗(yàn)證的責(zé)任也交給服務(wù)器。
- If-Modified-Since:和 Last-modified 比較,是否已經(jīng)修改了。
- If-None-Match:和 ETag 比較,唯一標(biāo)識(shí)是否一致。
- If-Unmodified-Since:和 Last-modified 對(duì)比,是否修改。
- If-Match:和 ETag 比較是否匹配。
- If-Range。
其中,最常見的當(dāng)屬是 If-Modified-Since 和 If-None-Match。它們分別對(duì)應(yīng)Last-Modified 和 ETag。需要第一次的響應(yīng)報(bào)文預(yù)先提供 Last-Modified 和 ETag,然后第二次請(qǐng)求時(shí)就可以帶上緩存里的原址,驗(yàn)證資源是否是最新的。
如果資源沒有變,服務(wù)器就回應(yīng)一個(gè) 304 Not Modified ,表示緩存依然有效,瀏覽器就可以更新一個(gè)有效期,然后使用緩存了。
緩存流程
什么時(shí)候用強(qiáng)緩存,什么時(shí)候用協(xié)商緩存?
首先強(qiáng)緩存的權(quán)重大于協(xié)商緩存,當(dāng)強(qiáng)緩存存在時(shí),協(xié)商緩存只能看著;其次 HTTP/1.1 中的緩存標(biāo)識(shí)符大于 HTTP/1;所以當(dāng) Cache-Control 存在時(shí),看它的,如果它不存在,則看 Expires,如果將強(qiáng)緩存設(shè)置為 Cache-Control:no-cache、Cache-Control:max-age=0、pragma: no-cache,即告訴瀏覽器不走強(qiáng)緩存,則進(jìn)入?yún)f(xié)商緩存。
判斷上次響應(yīng)中是否有ETag,如果有,則發(fā)起請(qǐng)求,請(qǐng)求頭中帶有條件請(qǐng)求 If-None-Match,如果沒有,則再判斷上次響應(yīng)中是否有Last-Modified,如果有,則發(fā)起請(qǐng)求頭中帶If-Modified-Since 的條件請(qǐng)求,如果沒有,則說明沒有協(xié)商緩存,發(fā)起 HTTP 請(qǐng)求即可。無論是帶If-None-Match的請(qǐng)求還是 If-Modified-Since 的請(qǐng)求,都會(huì)返回狀態(tài)(由服務(wù)器端判讀資源是否變化),如果是304,說明緩存資源未變,使用本地緩存;如果是200,則說明資源改變,發(fā)起 HTTP 請(qǐng)求,并記住響應(yīng)頭中的 ETag/Last-Modified。
大致流程圖如下所示:
緩存判斷流程圖
那么哪些資源要采用強(qiáng)緩存,哪些資源采用協(xié)商緩存呢?
像靜態(tài)資源這類我們長期不會(huì)去變動(dòng)的資源應(yīng)該用強(qiáng)緩存,不難理解;而像我們常修改的文件應(yīng)該采用協(xié)商緩存,如果資源沒變,那么當(dāng)用戶第二次進(jìn)去還是用該資源,如果資源修改,用戶進(jìn)入發(fā)起 HTTP 請(qǐng)求獲取最新資源。
我們?cè)谠L問網(wǎng)站時(shí),如果留心都能在 F12 中觀察到一二。如圖所示,我的五年前端三年面試放在 github 服務(wù)器上,F(xiàn)12進(jìn)入 Network中,能看到返回頭中的信息。Cache-Control、Expires、ETag、Last-Modified都存在。
五年前端三年面試
緩存位置
上文中常提到無論使用強(qiáng)緩存還是協(xié)商緩存,都會(huì)從瀏覽器本地中獲取,那么瀏覽器的本地存儲(chǔ)是存在哪里,他們又有什么分類呢?
按照緩存位置分類,分為四處,Memory Cache(內(nèi)存緩存)、Disk Cache(硬盤緩存)、Service Worker、Push Cache。
Memory Cache
因?yàn)閮?nèi)存有限,并不是所有的資源文件都會(huì)放在內(nèi)存里緩存,它主要用來緩存有 preloader 相關(guān)指令的資源,比如。preloader 可以一邊解析 js/css 文件,一邊網(wǎng)絡(luò)請(qǐng)求下一個(gè)資源。
Disk Cache
磁盤上的緩存。在所有瀏覽器緩存中,disk cache 覆蓋面最大,它會(huì)根據(jù) HTTP Header 中的字段判斷哪些資源需要緩存,哪些資源已經(jīng)過期需要重新從服務(wù)器端請(qǐng)求。
Service Worker
獨(dú)立線程,借鑒了 Web Worker 的思路。即讓 JS 運(yùn)行在主線程之外,由于它脫離瀏覽器窗口,因?yàn)闊o法直接訪問DOM,但是它還是能做很多事情,如
- 離線緩存,Service Worker Cache。
- 消息推送。
- 網(wǎng)絡(luò)代理。
- 它是PWA的重要實(shí)現(xiàn)機(jī)制。
Push Cache
即推送緩存,瀏覽器中的最后一道防線,HTTP2中的內(nèi)容。
優(yōu)先級(jí):Service Worker-->Memory Cache-->Disk Cache-->Push Cache。
實(shí)踐
說了這么多理論知識(shí),等實(shí)戰(zhàn)的時(shí)候卻一頭霧水,怎么破?
以上皆為口舌之辯,唯有實(shí)踐出真章(以上皆為面試之辯,唯有實(shí)踐出本事)。
目前前端項(xiàng)目都是以 webpack 或類 webpack 工具庫打包,在 webpack 中配置哈希,前端方面的緩存工作就完成了。
我們要實(shí)現(xiàn)的效果是:
- HTML:協(xié)商緩存。
- CSS、JS、圖片等資源:強(qiáng)緩存,文件名帶上hash。
webpack 中的哈希有三種:hash、chunkHash、contentHash。
- Hash:和整個(gè)項(xiàng)目的構(gòu)建相關(guān),只要項(xiàng)目文件有改變,整個(gè)項(xiàng)目構(gòu)建的 hash 值就會(huì)改變。
- chunkHash:和 webpack 打包的 chunk 有關(guān),不同的入口會(huì)生成不同的 chunkHash值。
- contentHash:根據(jù)文件內(nèi)容來定義hash,文件內(nèi)容不變,則 contentHash 不變。
這邊需要把 CSS 用 contentHash 處理,其他資源用 chunkHash 做處理。
非前端工程化項(xiàng)目
即傳統(tǒng)的前端頁面,一般放在靜態(tài)服務(wù)器中,那么就要對(duì)修改的文件做版本控制,例如在入口文件 index.js 上加版本號(hào)(index-v2.min.js)或者加時(shí)間戳(time=1626226),以此做緩存策略。
后端緩存實(shí)踐
真正起到緩存作用的是在后端,后端來設(shè)置緩存策略,告訴瀏覽器能否做緩存。這里我們對(duì)強(qiáng)緩存和協(xié)商緩存做個(gè)demo來實(shí)驗(yàn)下。
強(qiáng)緩存方案
代碼如下:
const express = require('express');
const app = express();
var options = {
etag: false, // 禁用協(xié)商緩存
lastModified: false, // 禁用協(xié)商緩存
setHeaders: (res, path, stat) => {
res.set('Cache-Control', 'max-age=10'); // 強(qiáng)緩存超時(shí)時(shí)間為10秒
},
};
app.use(express.static((__dirname + '/public'), options));
app.listen(3008);
PS:代碼來源自:圖解 HTTP 緩存,在做測(cè)試時(shí),需要注意,強(qiáng)緩存下,刷新頁面是測(cè)不出來,點(diǎn)擊后返回方能有效。
強(qiáng)緩存效果
協(xié)商緩存方案
代碼如下:
const express = require('express');
const app = express();
var options = {
etag: true, // 開啟協(xié)商緩存
lastModified: true, // 開啟協(xié)商緩存
setHeaders: (res, path, stat) => {
res.set({
'Cache-Control': 'max-age=00', // 瀏覽器不走強(qiáng)緩存
'Pragma': 'no-cache', // 瀏覽器不走強(qiáng)緩存
});
},
};
app.use(express.static((__dirname + '/public'), options));
app.listen(3001);
效果如下:
協(xié)商緩存效果
總結(jié)
HTTP 為什么要緩存,為了分擔(dān)服務(wù)器壓力,也為了讓頁面加載更快。
有什么手段?HTTP 的強(qiáng)緩存和協(xié)商緩存,強(qiáng)緩存作用于那些不怎么變化的資源(如引入的庫,js,css等),協(xié)商緩存適用常更新的文件(例如 html)。
強(qiáng)緩存是什么?在 HTTP/1.0 中以 Expires 為依據(jù),但它不準(zhǔn)確,HTTP 協(xié)議升級(jí)成1.1后,用新標(biāo)識(shí)符 Cache-Control 來代替,但兩者可以同時(shí)存在,Cache-Control 的權(quán)重更大一些。
協(xié)商緩存是什么?在 HTTP/1.0 中以 Last-Modified 為依據(jù),即最后過期修改時(shí)間,它也不準(zhǔn)確,HTTP升級(jí)成1.1后,用新標(biāo)識(shí)符 ETag 來代替,兩者可同時(shí)存在,后者的權(quán)重更大。
無論是 Expires ,還是 Last-Modified,都是以時(shí)間點(diǎn)來依據(jù),理論上是不出問題,但卻出問題了,所以就有了新的方案。
其中強(qiáng)緩存存在時(shí),瀏覽器會(huì)采用強(qiáng)緩存標(biāo)識(shí)符來緩存,當(dāng)將強(qiáng)緩存設(shè)置為失效時(shí),瀏覽器則會(huì)采用協(xié)商緩存來做緩存策略。
以上,即使筆者所理解的 HTTP 緩存。