筆者在剛開始寫網頁時,總是會遇到改了檔案重新整理畫面卻沒更新,或是更新了一張圖檔到伺服器但網頁內容沒有更換等情況,使得現在按 Ctrl/CMD + R 時,小指都會自動同時按下 Shift 來忽略快取;相信不少人多少都有類似的經驗吧?但到底什麼是快取呢?
本系列文已經重新編校彙整編輯成冊,並正式出版囉!
《前端三十:從 HTML 到瀏覽器渲染的前端開發者必備心法》好評販售中!
喜歡我文章內容的讀者們,歡迎您 前往購買 支持!
想像一下,假設有個人叫做小明,他每天都會照三餐滑 Instagram 配飯吃,滑滑按按,可能一下子就看個好幾十張照片,就先算他個 50 張好了;又因為小明沒有朋友,所以很容易就滑到重複的內容了,可能看的 50 張裡有一半是看過的舊的照片。
小明在滑的過程中,其實就等同於不斷地向 Instagram 拿到下一則動態,但如果這則動態是小明先前看過的,如果又重複向伺服器拿取,一來一往之間就浪費了不少網路傳輸量;快取就是幫我們省下這個浪費,將你看過的東西留下來,在需要時便能直接拿出來用,不用再次向資料來源端請求。
在 幾天前 我們聊到了網站優化,文中提到了全世界網站內容的資源量不斷增加,現在(2019/09)網站的平均資源下載量已經到達 5M 之譜;但如果使用者已經造訪過該網站,其實有不少資源都可以重複利用對吧?例如不會變動的靜態資料、頁面中的部分圖檔等等;這些資源,便可以透過一層層的快取機制,將內容留存住。
既然快取就是將內容留存留存一份,方便下次取用,那麼應該留在哪裡呢?
從網站使用的方向來思考吧。首先是使用者的電腦及瀏覽器,在送出請求前,會先確認是否有本地快取(也就是我們清除快取在清除的東西),如果沒有或是快取過期,瀏覽器才會真正送出請求。
除此之外,如同我們 先前 聊到的,透過 JavaScript 操作 Web Storage API,也是可以做為快取的儲存層;接下來,我們拿的資源如果被放在 CDN,那麼我們就會連到最近的 CDN 節點取得資源;這些 CDN 節點對於伺服器來說,也算是一種內容快取;另外,在請求傳遞到伺服器的過程中,可能會經過路由、代理伺服器、負載平衡器等設備,這些也都是可以暫存資料的地方;最後請求真的到伺服器時,伺服器如果需要向其他外部服務取得資源如 DB、API 等等,也可以透過快取的方式來儲存常用資料。
這麼多種快取方式,是不是有點頭大啊?其實一切的目標就只有一個:重複利用資源以避免浪費效能。例如像前述 Instagram 的例子,一個每天有超過 5 億使用者的服務,藉由妥善的快取機制,能省下多少流量資源,自然不在話下。
說了那麼多,但今天剩下的內容就聚焦在與前端較為相關的本地快取吧!
如同昨天說的 跨域問題,關於快取的設定也大都是由伺服器設定 Header 來進行控制。
最常見的大概是 Cache-Control
了,它有四個狀態:
public
:公開的資源,可以被所有節點暫存private
:私有的資源,只被允許儲存成使用者的本地快取no-cache
:本地暫存,但使用前會先詢問過期沒no-store
:不開快取其中,public
及 private
還可以設定 max-age=...
來指定快取多久後會過期;例如 Cache-Control: private max-age=2592000
,描述的是此資源僅可以被本地快取,有效期限是 30 天(60 x 60 x 24 x 30 = 2592000)
還有一個功能類似的設定:Expires
,算是歷史產物吧;但根據 規範 所述,Expires
的值會被 max-age=...
蓋掉;暫時可以不用理它。
定義了快取過期時間,但如同 有些食物即使過期了仍然能吃 一樣,過期了不見得就不能用;如果檔案沒有更改過,是不是就不用重新下載資源了呢?
沒有錯,在上圖的最後有個 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 曾說過這段話:
快取是優化效能的苦口良藥,控制得當能省下大量的資源,但要真的能完全控制得當就是一門大學問了;藉由 Cache-Control
及 ETag
兩個 Header 的設定,便可以將大部分的快取功能設置完成,如果真的有意外的更動,也可以透過打包工具變動檔名,讓使用者直接請求新資源。
那麼以上就是今天的快取淺淺談啦,若讀者您有任何疑問或不清楚的地方,都歡迎於底下留言討論。那麼前端篇也在今天告一段落啦,從明天開始又是全新的篇章,還請大家繼續鎖定本系列文,跟著筆者一起完成這趟旅程!
筆者
Gary
半路出家網站工程師;半生熟的前端加上一點點的後端。
喜歡音樂,喜歡學習、分享,也喜歡當個遊戲宅。相信一切安排都是最好的路。
想請問文章內容說的不管是 Cache-Control 或是 ETag,是不是都只能由 server 控制設定,前端唯一可以做的事只有像 檔名控制 這種呢
你說的沒錯喔!對 Client 端來說,無法直接知道網站資源更新了與否,也因此快取的機制只能仰賴伺服器給與的 header 資訊;檔名控制則是越過 header 描述的快取機制,因為要取得的目標資源不一樣了,自然也就不會再去找舊的快取。
其他前端能做的,大概就是用 <meta>
關閉頁面快取,或是 xhr
時指定 cache: false
,要求瀏覽器不對這個請求做快取等等,但這些方法都與優化網站效能背道而馳,只是迴避處理快取的問題而已,就沒有特別在文中描述了。
謝謝你的解惑,你的文章真的超有幫助的.尤其在準備面試過程 XD 超多必考題