剛開始寫網頁時,總是會遇到改了檔案重新整理畫面卻沒更新,或是更新了一張圖檔到伺服器但網頁內容沒有更換等情況,使得現在按 Ctrl/CMD + R 時,小指都會自動同時按下 Shift 來忽略快取。

但到底什麼是快取?

快取是什麼#

想像一下,假設有個人叫做小明,他每天都會照三餐滑 Instagram 配飯吃,可能一下子就看個好幾十張照片。又因為小明沒有朋友,所以很容易就滑到重複的內容了。

小明在滑的過程中,其實就等同於不斷地向 Instagram 拿到下一則動態。但如果這則動態是小明先前看過的,又重複向伺服器拿取,一來一往之間就浪費了不少網路傳輸量。

快取就是幫我們省下這個浪費,將你看過的東西留下來,在需要時便能直接拿出來用,不用再次向資料來源端請求。

如果使用者已經造訪過該網站,其實有不少資源都可以重複利用,例如不會變動的靜態資料、頁面中的部分圖檔等等。這些資源便可以透過一層層的快取機制,將內容留存住。

快取的分層#

既然快取就是將內容留存一份,方便下次取用,那麼應該留在哪裡?

從網站使用的方向來思考。首先是使用者的電腦及瀏覽器,在送出請求前,會先確認是否有本地快取(也就是清除快取在清除的東西),如果沒有或是快取過期,瀏覽器才會真正送出請求。

除此之外,透過 JavaScript 操作 Web Storage API,也可以做為快取的儲存層。接下來,拿的資源如果被放在 CDN,那麼就會連到最近的 CDN 節點取得資源;這些 CDN 節點對於伺服器來說,也算是一種內容快取。

另外,在請求傳遞到伺服器的過程中,可能會經過路由、代理伺服器、負載平衡器等設備,這些也都是可以暫存資料的地方。最後請求真的到伺服器時,伺服器如果需要向其他外部服務取得資源如 DB、API 等等,也可以透過快取的方式來儲存常用資料。

一切的目標就只有一個:重複利用資源以避免浪費效能。

網頁的快取機制#

接下來就聚焦在與前端較為相關的本地 HTTP 快取(HTTP Cache)。如同跨域問題,關於快取的設定也大都是由伺服器設定 Header 來進行控制。

Cache-Control#

最常見的大概是 Cache-Control 了,它有四個狀態:

  • public:公開的資源,可以被所有節點暫存
  • private:私有的資源,只被允許儲存成使用者的本地快取
  • no-cache:本地暫存,但使用前會先詢問過期沒
  • no-store:不開快取

其中,publicprivate 還可以設定 max-age=... 來指定快取多久後會過期。

例如 Cache-Control: private max-age=2592000,描述的是此資源僅可以被本地快取,有效期限是 30 天(60 x 60 x 24 x 30 = 2592000)。

還有一個功能類似的設定 Expires,算是歷史產物。但根據規範所述,Expires 的值會被 max-age=... 蓋掉,暫時可以不用理它。

這種透過 Cache-ControlExpires 直接控制快取過期時間的機制,稱為強快取(Strong Cache)。

ETag#

定義了快取過期時間,但過期了不見得就不能用。如果檔案沒有更改過,是不是就不用重新下載資源?

這就是 ETag 屬性的用途,也是協商快取(Conditional Cache)的核心機制。

開發者可以在 Header 設定 ETag,值為檔案計算後的 hash 值。由於同一份檔案由同一個伺服器算出來的 ETag 會完全相同,當使用者快取過期,在重新請求資源時便會帶著 ETag,伺服器重新計算後,如果 ETag 值沒有變動,表示檔案沒有更動,將回傳 304,表示檔案未更動,可以直接繼續使用。

檔名控制#

前面提到,可以藉由 max-age=... 來指定快取的有效期限。但如果今天某資源在有效期限內有更動,前述的快取機制便會忽略掉這個更動,導致資源版本沒有同步。

這樣的問題,可以在程式的檔名加上類似 ETag 的 hash 值,當版本更新、檔案名稱更動,瀏覽器自然也會忽略快取,直接向新資源發送請求。

但總不可能每次修改都手動改檔名,這時候就需要打包工具。例如最熱門的 Webpack,只需要在 output filename 的地方設定 contenthash,就可以快速地加上 hash 值:

// webpack.config.js
module.exports = {
  entry: "./src/index.js",
  plugins: [
    new HtmlWebpackPlugin({
      title: "Output Management",
      title: "Caching",
    }),
  ],
  output: {
    filename: "[name].[contenthash].js",
    path: path.resolve(__dirname, "dist"),
  },
};

如此一來,藉由 Header 及檔名的雙重控制,開發者便能完全控制使用者取得資源的快取方式。

結語#

Stack Overflow 的共同創辦人 Jeff Atwood 曾說過:

There are two hard things in computer science: cache invalidation, naming things, and off-by-one errors.

快取是優化效能的苦口良藥,控制得當能省下大量的資源,但要真的能完全控制得當就是一門大學問。藉由 Cache-ControlETag 兩個 Header 的設定,便可以將大部分的快取功能設置完成;如果真的有意外的更動,也可以透過打包工具變動檔名,讓使用者直接請求新資源。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10226607