iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 23
1

tag

筆者在剛開始寫網頁時,總是會遇到改了檔案重新整理畫面卻沒更新,或是更新了一張圖檔到伺服器但網頁內容沒有更換等情況,使得現在按 Ctrl/CMD + R 時,小指都會自動同時按下 Shift 來忽略快取;相信不少人多少都有類似的經驗吧?但到底什麼是快取呢?

快取是什麼

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

小明在滑的過程中,其實就等同於不斷地向 Instagram 拿到下一則動態,但如果這則動態是小明先前看過的,如果又重複向伺服器拿取,一來一往之間就浪費了不少網路傳輸量;快取就是幫我們省下這個浪費,將你看過的東西留下來,在需要時便能直接拿出來用,不用再次向資料來源端請求。

幾天前 我們聊到了網站優化,文中提到了全世界網站內容的資源量不斷增加,現在(2019/09)網站的平均資源下載量已經到達 5M 之譜;但如果使用者已經造訪過該網站,其實有不少資源都可以重複利用對吧?例如不會變動的靜態資料、頁面中的部分圖檔等等;這些資源,便可以透過一層層的快取機制,將內容留存住。

快取的分層

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

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

cache

除此之外,如同我們 先前 聊到的,透過 JavaScript 操作 Web Storage API,也是可以做為快取的儲存層;接下來,我們拿的資源如果被放在 CDN,那麼我們就會連到最近的 CDN 節點取得資源;這些 CDN 節點對於伺服器來說,也算是一種內容快取;另外,在請求傳遞到伺服器的過程中,可能會經過路由、代理伺服器、負載平衡器等設備,這些也都是可以暫存資料的地方;最後請求真的到伺服器時,伺服器如果需要向其他外部服務取得資源如 DB、API 等等,也可以透過快取的方式來儲存常用資料。

這麼多種快取方式,是不是有點頭大啊?其實一切的目標就只有一個:重複利用資源以避免浪費效能。例如像前述 Instagram 的例子,一個每天有超過 5 億使用者的服務,藉由妥善的快取機制,能省下多少流量資源,自然不在話下。

網頁的快取機制

說了那麼多,但今天剩下的內容就聚焦在與前端較為相關的本地快取吧!

如同昨天說的 跨域問題,關於快取的設定也大都是由伺服器設定 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=... 蓋掉;暫時可以不用理它。

ETag

定義了快取過期時間,但如同 有些食物即使過期了仍然能吃 一樣,過期了不見得就不能用;如果檔案沒有更改過,是不是就不用重新下載資源了呢?

沒有錯,在上圖的最後有個 ETag 屬性,就是來解決這個需求的。

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

圖片來自 HTTP Cat <3

檔名控制

前面提到,可以藉由 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 的設定,便可以將大部分的快取功能設置完成,如果真的有意外的更動,也可以透過打包工具變動檔名,讓使用者直接請求新資源。

那麼以上就是今天的快取淺淺談啦,若讀者您有任何疑問或不清楚的地方,都歡迎於底下留言討論。那麼前端篇也在今天告一段落啦,從明天開始又是全新的篇章,還請大家繼續鎖定本系列文,跟著筆者一起完成這趟旅程!

參考資料

筆者

Gary

半路出家網站工程師;半生熟的前端加上一點點的後端。
喜歡音樂,喜歡學習、分享,也喜歡當個遊戲宅。

相信一切安排都是最好的路。


上一篇
22. [FE] 為什麼跨域請求會產生錯誤?如何處理?
下一篇
24. [BE] 請說明一下 npm 的套件管理機制。
系列文
前端三十 - 成為更好的前端工程師31

1 則留言

1
hannahpun
iT邦新手 5 級 ‧ 2019-10-27 13:07:07

想請問文章內容說的不管是 Cache-Control 或是 ETag,是不是都只能由 server 控制設定,前端唯一可以做的事只有像 檔名控制 這種呢

Gary iT邦新手 5 級‧ 2019-10-27 17:28:18 檢舉

嗨,hannahpun

你說的沒錯喔!對 Client 端來說,無法直接知道網站資源更新了與否,也因此快取的機制只能仰賴伺服器給與的 header 資訊;檔名控制則是越過 header 描述的快取機制,因為要取得的目標資源不一樣了,自然也就不會再去找舊的快取。

其他前端能做的,大概就是用 <meta> 關閉頁面快取,或是 xhr 時指定 cache: false,要求瀏覽器不對這個請求做快取等等,但這些方法都與優化網站效能背道而馳,只是迴避處理快取的問題而已,就沒有特別在文中描述了。

Gary iT邦新手 5 級‧ 2019-10-27 17:32:31 檢舉

謝謝你的解惑,你的文章真的超有幫助的.尤其在準備面試過程 XD 超多必考題

我要留言

立即登入留言