面試常客:HTTP 快取

山頭人漢波發表於2022-05-02

速度、速度,還是速度,一個網站要想體驗好,就必須在第一時間以最快的速度顯示出來。mysql查詢慢,就加一層 redis 做快取,網站資源載入慢,怎麼做,使用 HTTP快取

HTTP快取自 HTTP/1.0 就開始有,為的是減少伺服器壓力,加快網頁響應速度

快取操作的目標

HTTP 快取只能儲存 GET 請求的響應,而對其他型別的請求無能為力

快取發展史

HTTP/1.0 提出快取概念,即強快取 Expires 和協商快取 Last-Modified。後 HTTP/1.1 又有了更好的方案,即強快取 Cache-Control 和協商快取 ETag

為什麼 Expires 和 Last-Modified 不適用呢?

Expires 即過期時間,但問題是這個時間點是伺服器的時間,如果客戶端的時間和伺服器時間有差,就不準確。所以用 Cache-Control 來代替,它表示過期時長,這就沒歧義了

Last-Modified 即最後修改時間,而它能感知的單位時間是秒,也就是說如果在1秒內改變多次,內容檔案雖然改變了,但展示還是之前的,存在不準確的場景,所以就有了 ETag,通過內容給資源打標識來判斷資源是否變化

以下表格利於對比理解

版本強快取協商快取
HTTP/1.0ExpiresLast-Modified
HTTP/1.1Cache-ControlETag

兩大快取型別對比

前文已介紹不同版本下的快取型別。當時提了有一句強快取和協商快取,但沒具體介紹。現在來講講這兩種快取型別

強快取

Cache-Control

  • HTTP/1.1
  • 通過過期時長控制快取,對應的欄位有很多,例如 max-age

    • 例如 Cache-Control: max-age=3600,表示快取時間為3600秒,過期失效
  • 快取請求指令:

    • Cache-Control: max-age=<seconds>
      Cache-Control: max-stale[=<seconds>]
      Cache-Control: min-fresh=<seconds>
      Cache-control: no-cache
      Cache-control: no-store
      Cache-control: no-transform
  • 快取響應指令:

    • 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=<seconds>
  • 其中關鍵點:

    • Cache-control: no-cache

      • 跳過當前的強快取,傳送 HTTP 請求(如有協商快取標識即直接進入協商快取階段
      • no-cache 的含義和 max-age=0 一樣 ,即跳過強快取,強制重新整理
    • Cache-control: no-store

      • 不使用快取(包括協商快取)
    • Cache-Control: public, max-age=31536000

      • 一般用於快取靜態資源
      • public:響應可以被中間代理、CDN 等快取
      • private:專用於個人的快取,中間代理、CDN等能換快取此響應
      • max-age:單位是秒
  • 更多指令參考指令大全

Expires

  • HTTP/1.0
  • 語法:

    • Expires: <http-date>
  • 即過期時間,存在於伺服器返回的響應頭裡

    • Expires: Mon, 11 Apr 2022 06:57:18 GMT
    • 表示資源在2022年4月11號6點57分過期,過期了就會往服務端發請求
  • 如果在Cache-Control響應頭設定了 "max-age" 或者 "s-max-age" 指令,那麼 Expires 頭會被忽略
  • 缺點:伺服器時間與瀏覽器時間可能不一致
  • 更多指令參考指令大全

Cache-Control VS Expires

  • Cache-Control 較之 Expires 更為精準
  • 同時存在時,Cache-Control 優先順序大於 Expires
  • Expires 是 HTTP/1.0 提出,其瀏覽器相容性更好,Cache-Control 是 HTTP/1.1 提出,可同時存在,當有不支援 Cache-Control 的瀏覽器時會以 Expires 為準

協商快取

協商快取需要配合強快取使用,使用協商快取的前提是設定強快取設定 Cache-Control: no-cache或者 pragma: no-cache或者 max-age=0 告訴瀏覽器不走強快取

pragma 是 HTTP/1.0 中禁止網頁快取的欄位,其取值為 no-cache 和 Cache-Control 的 no-cache 效果一樣

ETag/If-None-Match

  • HTTP/1.1
  • 即生成檔案唯一標識來判斷是否過期。只要內容改變,這個值就會變
  • If-None-Match 配合,ETag是請求伺服器後返回給每個資原始檔的唯一標識,客戶端會將此標識存在客戶端(即瀏覽器)中,下次請求時會在請求頭的 If-Nono-Match 中將其值帶上,伺服器判斷 If-None-Match 是否與自身伺服器上的 ETag 一致,如果一致則返回 304,重定向跳轉使用本地快取;不一致,則返回200,將最新資源返回給客戶端,並帶上 ETag
  • 更多指令參考指令大全

Last-Modified/If-Modified-Since

  • HTTP/1.0
  • 最後修改時間,即通過最後修改時間來判斷是否過期。在瀏覽器第一次給伺服器傳送請求後,伺服器會在響應頭上加上這個欄位
  • If-Modified-Since 配合,客戶端訪問伺服器資源時,伺服器端會將 Last-Modified 放入響應頭中,即這個資源在伺服器上的最後修改時間,客戶端快取這個值,等下次請求這個資源時,瀏覽器會檢測到請求頭中的 Last-Modified,於是乎新增 If-Modified-Since,如果 If-Modified-Since 的值與伺服器中這個資源的最後修改時間一致,則返回 304,重定向跳轉使用本地快取;不一致,則返回200,將最新資源返回給客戶端,並帶上 Last-Modified
  • 缺點:

    • 檔案雖然被修改,但最後的內容沒有變化,這樣檔案修改時間還是會更新
    • 有些檔案修改頻率在秒以內,這樣以秒粒度來記錄就不適用了
    • 有些伺服器無法精準獲取檔案的最後修改時間
  • 更多指令參考指令大全

ETag VS Last-Modified

  • 精確度

    • ETag > Last-Modified。ETag 是通過內容給資源打標識來判斷資源是否變化,而 Last-Modified不一樣,在某些場景下準確度會失效。例如編輯檔案,但是檔案內容未變,快取會失效;或者在1秒內改變多次,Last-Modified能感知的單位時間是秒
  • 效能

    • Last-Modified > ETag。Last-Modified 僅僅記錄一個時間點,而 ETag需要根據檔案的具體內容生成雜湊值
  • 如果兩個都支援的話,伺服器會優先選擇ETag

協商快取的條件請求

前文說到協商快取是在請求頭新增 If-None-MatchIf-Modified-Since,這些請求頭是什麼,新增有什麼用?

強快取是通過具體時間到期或過期時長來控制快取,這就有個問題了,如果其中的一些檔案修改了,因為強快取,瀏覽器展示的還是原來的資料,所以對那種常變化的資料不能使用強快取做快取策略,於是乎,就有了協商快取,通過檔案變化告訴瀏覽器快取失效,使用前需去伺服器驗證是否是最新版?

這樣,瀏覽器就要連續傳送兩個請求來驗證:

  1. 先是 HEAD 請求,獲取資源的修改時間、hash值等元資訊,然後與快取資料比較,如果沒有改動就使用快取
  2. 否則就再發一個 GET 請求,獲取最新的版本

但這樣的兩個請求的網路成本太高,所以 HTTP 協議就定義了一系列 If 開頭的條件請求欄位,專門用來檢查驗證資源是否過期,把兩個請求合併在一個請求中做。而且驗證的責任也交給伺服器

  • If-Modified-Since:和 Last-modified 比較,是否已經修改了
  • If-None-Match:和 ETag 比較,唯一標識是否一致
  • If-Unmodified-Since:和 Last-modified 對比,是否修改
  • If-Match:和 ETag 比較是否匹配
  • If-Range

其中,最常見的當屬是 If-Modified-Since 和 If-None-Match。它們分別對應Last-Modified 和 ETag。需要第一次的響應報文預先提供 Last-Modified 和 ETag,然後第二次請求時就可以帶上快取裡的原址,驗證資源是否是最新的

如果資源沒有變,伺服器就回應一個 304 Not Modified ,表示快取依然有效,瀏覽器就可以更新一個有效期,然後使用快取了

快取流程

什麼時候用強快取,什麼時候用協商快取?

首先強快取的權重大於協商快取,當強快取存在時,協商快取只能看著;其次 HTTP/1.1 中的快取識別符號大於 HTTP/1;所以當 Cache-Control 存在時,看它的,如果它不存在,則看 Expires,如果將強快取設定為 Cache-Control:no-cacheCache-Control:max-age=0pragma: no-cache,即告訴瀏覽器不走強快取,則進入協商快取。

判斷上次響應中是否有ETag,如果有,則發起請求,請求頭中帶有條件請求 If-None-Match,如果沒有,則再判斷上次響應中是否有Last-Modified,如果有,則發起請求頭中帶If-Modified-Since 的條件請求,如果沒有,則說明沒有協商快取,發起 HTTP 請求即可。無論是帶If-None-Match的請求還是 If-Modified-Since 的請求,都會返回狀態(由伺服器端判讀資源是否變化),如果是304,說明快取資源未變,使用本地快取;如果是200,則說明資源改變,發起 HTTP 請求,並記住響應頭中的 ETag/Last-Modified

大致流程圖如下所示:

快取判斷流程圖

那麼哪些資源要採用強快取,哪些資源採用協商快取呢?

像靜態資源這類我們長期不會去變動的資源應該用強快取,不難理解;而像我們常修改的檔案應該採用協商快取,如果資源沒變,那麼當使用者第二次進去還是用該資源,如果資源修改,使用者進入發起 HTTP 請求獲取最新資源

我們在訪問網站時,如果留心都能在 F12 中觀察到一二。如圖所示,我的五年前端三年面試放在 github 伺服器上,F12進入 Network中,能看到返回頭中的資訊。Cache-Control、Expires、ETag、Last-Modified都存在

五年前端三年面試

快取位置

上文中常提到無論使用強快取還是協商快取,都會從瀏覽器本地中獲取,那麼瀏覽器的本地儲存是存在哪裡,他們又有什麼分類呢?

按照快取位置分類,分為四處,Memory Cache(記憶體快取)、Disk Cache(硬碟快取)、Service Worker、Push Cache

Memory Cache

因為記憶體有限,並不是所有的資原始檔都會放在記憶體裡快取,它主要用來快取有 preloader 相關指令的資源,比如<link rel="prefetch">。preloader 可以一邊解析 js/css 檔案,一邊網路請求下一個資源

Disk Cache

磁碟上的快取。在所有瀏覽器快取中,disk cache 覆蓋面最大,它會根據 HTTP Header 中的欄位判斷哪些資源需要快取,哪些資源已經過期需要重新從伺服器端請求

Service Worker

獨立執行緒,借鑑了 Web Worker 的思路。即讓 JS 執行在主執行緒之外,由於它脫離瀏覽器視窗,因為無法直接訪問DOM,但是它還是能做很多事情,如

  • 離線快取,Service Worker Cache
  • 訊息推送
  • 網路代理
  • 它是PWA的重要實現機制

Push Cache

即推送快取,瀏覽器中的最後一道防線,HTTP2中的內容

優先順序:Service Worker-->Memory Cache-->Disk Cache-->Push Cache。

實踐

說了這麼多理論知識,等實戰的時候卻一頭霧水,怎麼破?

以上皆為口舌之辯,唯有實踐出真章(以上皆為面試之辯,唯有實踐出本事)

目前前端專案都是以 webpack 或類 webpack 工具庫打包,在 webpack 中配置雜湊,前端方面的快取工作就完成了

我們要實現的效果是:

  • HTML:協商快取
  • CSS、JS、圖片等資源:強快取,檔名帶上hash

webpack 中的雜湊有三種:hash、chunkHash、contentHash

  • Hash:和整個專案的構建相關,只要專案檔案有改變,整個專案構建的 hash 值就會改變
  • chunkHash:和 webpack 打包的 chunk 有關,不同的入口會生成不同的 chunkHash值
  • contentHash:根據檔案內容來定義hash,檔案內容不變,則 contentHash 不變

這邊需要把 CSS 用 contentHash 處理,其他資源用 chunkHash 做處理

非前端工程化專案

即傳統的前端頁面,一般放在靜態伺服器中,那麼就要對修改的檔案做版本控制,例如在入口檔案 index.js 上加版本號(index-v2.min.js)或者加時間戳(time=1626226),以此做快取策略

後端快取實踐

真正起到快取作用的是在後端,後端來設定快取策略,告訴瀏覽器能否做快取。這裡我們對強快取和協商快取做個demo來實驗下,

強快取方案

程式碼如下:

const express = require('express');
const app = express();
var options = { 
  etag: false, // 禁用協商快取
  lastModified: false, // 禁用協商快取
  setHeaders: (res, path, stat) => {
    res.set('Cache-Control', 'max-age=10'); // 強快取超時時間為10秒
  },
};
app.use(express.static((__dirname + '/public'), options));
app.listen(3008);
PS:程式碼來源自:圖解 HTTP 快取,在做測試時,需要注意,強快取下,重新整理頁面是測不出來,點選後返回方能有效

強快取效果

協商快取方案

程式碼如下:

const express = require('express');
const app = express();
var options = {
    etag: true, // 開啟協商快取
    lastModified: true, // 開啟協商快取
    setHeaders: (res, path, stat) => {
        res.set({
            'Cache-Control': 'max-age=00', // 瀏覽器不走強快取
            'Pragma': 'no-cache', // 瀏覽器不走強快取
        });
    },
};
app.use(express.static((__dirname + '/public'), options));
app.listen(3001);

效果如下:

協商快取效果

總結

HTTP 為什麼要快取,為了分擔伺服器壓力,也為了讓頁面載入更快

有什麼手段?HTTP 的強快取和協商快取,強快取作用於那些不怎麼變化的資源(如引入的庫,js,css等),協商快取適用常更新的檔案(例如 html)

強快取是什麼?在 HTTP/1.0 中以 Expires 為依據,但它不準確,HTTP 協議升級成1.1後,用新識別符號 Cache-Control 來代替,但兩者可以同時存在,Cache-Control 的權重更大一些

協商快取是什麼?在 HTTP/1.0 中以 Last-Modified 為依據,即最後過期修改時間,它也不準確,HTTP升級成1.1後,用新識別符號 ETag 來代替,兩者可同時存在,後者的權重更大

無論是 Expires ,還是 Last-Modified,都是以時間點來依據,理論上是不出問題,但卻出問題了,所以就有了新的方案

其中強快取存在時,瀏覽器會採用強快取識別符號來快取,當將強快取設定為失效時,瀏覽器則會採用協商快取來做快取策略

以上,即使筆者所理解的 HTTP 快取

附上demo地址

參考資料

相關文章