1. 程式人生 > 實用技巧 >使用 HTTP 快取:Etag, Last-Modified 與 Cache-Control

使用 HTTP 快取:Etag, Last-Modified 與 Cache-Control

整個 Web 系統架構在HTTP 協議之上, 利用 HTTP 的快取機制不僅可以極大地減少伺服器負載, 更重要的是加速頁面的載入,以及減少使用者的流量消耗。 快速到達和易於訪問是 Web 與生俱來的特性, 其快取機制也早已被伺服器和瀏覽器廠商廣泛地實現, 我們作為 Web 內容的作者何樂而不為呢?

Web 伺服器(比如 Tomcat、Apache、Virgo)或伺服器端框架(比如 Django、Express.js) 都會實現 HTTP 快取機制,但本文不借助這些框架, 而是直接以基本的 Node.js 程式與 Chrome 瀏覽器來描述 HTTP 中最基本的快取機制, 涉及到的 HTTP頭欄位

包括Cache-Control,Last-Modified,If-Modified-Since,Etag,If-None-Match等。

除 HTTP 快取之外,Web 效能優化還有很多其他途徑,比如預載入和預渲染指令碼非同步載入等。




HTTP 快取簡介

談起 HTTP 快取你首先想到的一定是磁碟快取,以及 304狀態碼。 這是瀏覽器處理快取的兩種情況:

  • 瀏覽器詢問伺服器快取是否有效,伺服器返回 304 指示瀏覽器使用快取。
  • 資源仍然處於有效期時,瀏覽器會直接使用磁碟快取(在重新整理時稍有不同,見下文)。

圖中favicon.ico直接來自磁碟快取,而localhost文件則來自 304 快取。 上述行為中涉及到 3 個 HTTP 響應頭欄位:

  • Cache-Control響應頭表示了資源是否可以被快取,以及快取的有效期。
  • Etag響應頭標識了資源的版本,此後瀏覽器可據此進行快取以及詢問伺服器。
  • Last-Modified響應頭標識了資源的修改時間,此後瀏覽器可據此進行快取以及詢問伺服器。

Cache-Control

Cache-Control在 HTTP 響應頭中,用於指示代理和 UA 使用何種快取策略(感謝Kingsley Chen指出此前對 no-cache 描述的錯誤)。比如:

  • no-cache為本次響應不可直接用於後續請求(在沒有向伺服器進行校驗的情況下)
  • no-store為禁止快取(不得儲存到非易失性介質,如果有的話儘量移除,用於敏感資訊)
  • private為僅 UA 可快取
  • public為大家都可以快取。

Cache-Control為可快取時,同時可指定快取時間(比如public, max-age:86400)。 這意味著在 1 天(60x60x24=86400)時間內,瀏覽器都可以直接使用該快取(此時伺服器收不到任何請求)。 當然瀏覽器也有權隨時丟棄任何一項快取,因此這裡可能有一致性問題。 注意下圖中狀態碼附近的from disk cache標識。

其伺服器程式碼如下:

import http from 'http'

let server = http.createServer((req, res) => {
  res.setHeader('Cache-Control', 'public, max-age=86400')
  res.end('harttle.land')
})

server.listen(3333)

除了Cache-Control中的max-age外,ExpiresVary等頭欄位也可用來設定快取的有效性。

Etag

如果資源本身確實會隨時發生改動,還用Cache-Control就會使使用者看到的頁面得不到更新。 但如果還希望利用 HTTP 快取(萬一資源沒變呢),這就需要有條件的(conditional)HTTP 請求。

Etag響應頭欄位表示資源的版本,瀏覽器在傳送請求時會帶If-None-Match頭欄位, 來詢問伺服器該版本是否仍然可用。如果伺服器發現該版本仍然是最新的, 就可以返回 304 狀態碼指示 UA 繼續使用快取。注意下圖中的If-None-Match欄位。

其伺服器端程式碼如下:

import http from 'http'

let server = http.createServer((req, res) => {
  console.log(req.url, req.headers['if-none-match'])
  if (req.headers['if-none-match']) {
    // 檢查檔案版本
    res.statusCode = 304
    res.end()
  }
  else {
    res.setHeader('Etag', '00000000')
    res.end('harttle.land')
  }
})

server.listen(3333)

Last-Modified

Etag類似,Last-ModifiedHTTP 響應頭也用來標識資源的有效性。 不同的是使用修改時間而不是實體標籤。對應的請求頭欄位為If-Modified-Since, 見下圖:

其伺服器端程式碼如下:

import http from 'http'

let server = http.createServer((req, res) => {
  console.log(req.url, req.headers['if-modified-since'])
  if (req.headers['if-modified-since']) {
    // 檢查時間戳
    res.statusCode = 304
    res.end()
  }
  else {
    res.setHeader('Last-Modified', new Date().toString())
    res.end('harttle.land')
  }
})

server.listen(3333)

瀏覽器重新整理

撰寫這篇文章的過程中,Harttle 使用了很多 Chrome 瀏覽器的截圖。 如果你使用瀏覽器除錯,可能也需要了解重新整理按鈕的行為。

正常重新載入

按下重新整理按鈕或快捷鍵(在 MacOS 中是 Cmd+R)會觸發瀏覽器的“正常重新載入”(normal reload), 此時瀏覽器會執行一次Conditional GETCache-Control等快取頭欄位會被忽略,並且帶If-None-Match,If-Modified-Since等頭欄位。 此時伺服器總會收到一次 HTTP GET 請求。 在 Chrome 中按下重新整理,瀏覽器還會帶如下請求頭:

Cache-Control:max-age=0

注意:在位址列重新輸入當前頁面地址並按下回車也會當做重新整理處理, 這意味著只有從新標籤頁或超連結開啟時,才能觀察到直接使用硬碟快取的情況。

強制重新載入

在 Chrome 中按下 Cmd+Shift+R (MacOS)可以觸發強制重新載入(Hard Reload), 此時包括頁面本身在內的所有資源都不會使用快取。 瀏覽器直接傳送 HTTP 請求且不帶任何條件請求欄位。




https://harttle.land/2017/04/04/using-http-cache.html