請解釋 HTTP caching 機制
2022年9月28日
如果要加快網頁應用程式的速度,caching 是個經常被用的策略 (caching 中文有被翻譯成緩存或快取,但因為工作與面試時都還是會說 caching 居多,這篇就暫不翻譯這個詞了)。當我們已經跟後端請求過某個資源,例如某筆資料或某張圖片,下一次再次請求時,如果該資源沒有改變,這時再次請求會相對浪費網路頻寬;反之,如果第一次請求來的資源已經被存下來,那麼下次請求時,可以直接用該資源,這樣可以減少不必要的請求。而這也是 caching 的概念。
caching 可以被應用在很多地方,AWS 的這篇文章中有概略分析到,在客戶端、DNS、伺服器、資料庫等地方都可以做 caching。而身為前後端工程師,在面試中很常被問到的是 HTTP 的 caching 機制。透過本篇文章,希望讓大家下次面試時被問到「請說明 HTTP caching 機制」時,可以解釋地夠清楚與完整。
HTTP caching 是用在哪? 為什麼要用 HTTP caching?
可以把 cache 理解成某個我們暫時存放資源 (例如某筆資料、某張圖片) 的地方,所以當下次需要這些資源時,不用再請求一次,而是可以直接從 cache 這個暫存處拿到。換到 HTTP caching 的脈絡,這個暫存的地方就是瀏覽器。舉例來說,當今天使用者逛了 LV 的官網,官網中的商品圖片與價錢,不太會快速改變,換句話說現在逛跟一小時後逛,看到的資訊很可能是完全一樣的。
這時當第一次逛網站時,前端跟伺服器請求了這些商品圖片、描述與價錢,把他們 cache 起來 (放在瀏覽器記憶體的某個地方),當使用者下次逛的時候,就不需要再跟伺服器請求了。下面這張是來自 MDN 的圖示,可以看到,如果沒有 cache,每一次請求都要對到伺服器;然而如果有 cache,則可以從 cache 裡面拿,可以減少直接對伺服器的請求
讀到這邊我們可以歸納出,這麼做有幾項 caching 好處,也是我們為什麼要用 caching 的理由
- 減少請求次數:因為不用請求,而是直接從 cache 拿出之前暫存的資料,這樣做能減少伺服器與資料庫端的負擔。
- 加快資源載入:向伺服器請求,需要等網路傳輸資料。直接從瀏覽器裡面的 cache 拿,就不用等這一段資料傳輸的時間,會快很多。
該如何設定 HTTP caching
上面談到 HTTP caching 的好處,以及可以把跟伺服器請求來的資料 cache 在瀏覽器中。但實際上該如何設定呢? 這也是面試時會被追問的。以下有幾種方式:
Expires
第一種方式是在 HTTP Response header 當中加入 Expires
,舉例來說:
Expires: Tue, 18 Jul 2022 16:07:23 GMT
瀏覽器收到該回應的資料會先把資料存在 cache 當中,而下一次用戶發送相同請求時,瀏覽器會去判斷現在時間是否已經到了 Expires
設定的時間,如果還沒到,那就會直接從 cache 裡面拿資料,而不是發送請求。
cache-control
由於 Expires
是比較舊的方法,現在比較少人會用,更多人會用 cache-control
。cache-control
的設定方式不是直接設定一個 cache 過期的時間點,而是設定 cache 有效的時間。舉例來說,下面這段是設定 cache 有效期是 60 秒。所以在第一次請求拿到回應後的六十秒內,如果在發送相同請求,瀏覽器都會直接拿 cache 的資料,而不是發請求到伺服器端
cache-control: max-age=60
cache-control 快問快答
關於 cache-control 的設定,有一些面試常會被問的快問快答,以下列出題目。下面會有答案,不過大家可以先自己想想看,看看自己知不知道這些問題的回答。
- 如果只想讓客戶端 cached,而不想讓中間層的代理伺服器等其他層 cached,該用什麼?
- 反之,如果想讓代理伺服器也能夠 cached 從後端來的資料,該用什麼?
- 因為很多時候瀏覽器可能會自動 cache,如果完全不想要有 cache,想要內容一直都是最新的,那又該用什麼
cache-control: no-store
跟cache-control: no-cache
兩者有什麼差別?
上面的問題的回答分別是
cache-control: private
cache-control: public
cache-control: no-store
cache-control: no-store
是指不要 cache,而cache-control: no-cache
則是指會 cache,不過每一次請求時都要重新驗證一次 (revalidate),換句話說每次都還是會問伺服器內容有沒有更新,沒更新就用 cache 的。詳見這篇討論。
HTTP caching 過期後,該如何重新驗證?
上面提到我們可以透過 cache-control: max-age
來設定多久後 cache 過期;不過當 cache 過期後就要直接跟伺服器請求嗎? 如果 cache 過期了,但其實伺服器那邊的資料並沒有更新,換句話說 cache 還是可以繼續被使用,這時有沒有什麼方法可以避免我們直接重新請求,繼續使用 cache? 有的,這又被稱為驗證 (validation)。而 HTTP caching 有兩種主要方式可以做到這件事。
ETag (搭配 If-None-Match)
第一個方式是在回應的 header 當中放入 ETag (entity tag 的簡寫)。這個 ETag 會是一個獨特的值,例如 ETag: "686897696a7c876b7e"
;如果後端的資料有變動,則 ETag 會改變。如果伺服器在回傳的 header 中有放入 ETag, 則之後瀏覽器在請求時,會在請求的 header 帶上 If-None-Match
欄位,而欄位的值會是之前收到的 ETag 的獨特值。
這時後端收到了該請求,並去查看 If-None_match
當中的 ETag 跟現在的 ETag 是不是一樣的。如果是一樣的,就代表後端的資料沒變 (因為如果資料有變,則 ETag 會跟著變);這時只需用傳個 304 Not Modified 給前端,瀏覽器收到 304 後,就知道資料沒變,所以可以繼續用 cache 的。而如果後端比較了 ETag 發現改變,那就不是回傳 304,而是回傳一包新的資料。
Last-Modified (搭配 If-Modified-Since)
第二個方式則是在伺服器的回應 header 中加入 Last-Modified
,並標注最後修改該資源的時間,例如 Last-Modified: 2021-11-07 21:32:16
。當瀏覽器收到帶有 Last-Modified
的回應後,之後的請求就會帶上 If-Modified-Since
,然後帶上先前收到的時間,例如 If-Modified-Since: 2021-11-07 21:32:16
。伺服器收到帶有 If-Modified-Since
的請求,比對了時間,如果更新資源的時間沒有變,那一樣可以回傳 304 Not Modified 給前端,如果變了則回傳 200 以及新的資料。