[食譜好菜] ASP.NET MVC 應該多多利用瀏覽器的 Cache

長期在 ASP.NET 打滾,講到 Cache 第一時間就會想到 Redis、Memcached、... 這種伺服器端的 Cache 服務,但是在 Web 技術領域內還有瀏覽器端的 Cache,如果沒有特別指定,檯面上這些主流的瀏覽器都會把 Web 伺服器回應的內容存起來,我們應該要好好地利用它們來降低伺服器跟網路的壓力。

要駕馭瀏覽器的 Cache 需要充分了解 HTTP 的 Cache 機制,推薦各位朋友率先閱讀《循序漸進理解 HTTP Cache 機制》這篇文章,講得很清楚。

到期時間掌控著要不要發送請求

使用瀏覽器 Cache 的原則就是只要瀏覽器的 Cache 還是新鮮的就取用,因此最理想的情況是在瀏覽器 Cache 還是新鮮的狀況之下,避免向伺服器發送請求而直接取用,這件事情由一個東西掌控著-時間

Expires

從 HTTP 1.0 就有的 Response Header-Expires,只要指定一個過期時間,在過期時間到期之前瀏覽器會優先取用 Cache 資料而不發送資源請求。

我先來介紹一下我簡單的實驗環境,我有一個頁面,這個頁面會載入一段 Scripts 將伺服器的現在時間顯示到畫面上。

而這段 Scripts 我從後端吐,好讓我方便調整 Response Header 來測試瀏覽器的 Cache 機制,所以瀏覽器有沒有使用 Cache 內容,從畫面上的時間就可以知道了。

我將 Scripts 加上 Expires Response Header,指定當前時間加上 5 秒為值。

可以看到不停地刷新頁面,畫面上的時間依舊停留在 xx:58:06,當超過 Expires 所設置的 xx:58:11 時,才又跳新的時間,在到達 Expires 所設置的時間之前,並不會向伺服器發送 /Cache/Scripts 的資源請求,而是一直使用瀏覽器先前 Cache 的內容。

Cache-Control: max-age

當 HTTP 來到 1.1 版本的時候,多了一個 Header-Cache-Control,而我們可以在 Cache-Control 設置 max-age 的值來做到跟 Expires 一樣的效果。

可以看到 Cache-Control 設置了 max-age 之後,一樣可以讓瀏覽器取用 Cache 內容而不必向伺服器發送資源請求。

Cache-Control: max-age=[n] 會覆蓋 Expires 的設定,所以兩者儘量不要混用。

資源內容不變的話可以不用再傳輸一次

剛剛我們已經知道了當 Cache 過期時,瀏覽器會向伺服器發送資源請求,如果伺服器在接收到請求後發現,其實資源的內容並沒有異動,瀏覽器就可以直接繼續用 Cache 的內容就好,那麼伺服器如何來告知瀏覽器這件事?以及伺服器如何得知資源內容沒有異動?

Last-Modified/If-Modified-Since

資源內容有沒有異動,伺服器怎麼知道? 我們可以從資源的最後修改時間來判斷得知,當瀏覽器首次向伺服器發送資源請求的時候,伺服器在 Response Header 裡面加入 Last-Modified 來跟瀏覽器說這個資源的最後修改時間,當 Cache 過期的時候,瀏覽器向伺服器發送資源請求時,在 Request Header 中加入 If-Modified-Since,其值就是當初伺服器回應的 Last-Modified。

當伺服器接收到 If-Modified-Since 的時候,就去與資源的最後修改時間做比對,如果不一樣就傳輸新的資源內容給瀏覽器,如果一樣就回應 304(Not Modified)給瀏覽器,告訴瀏覽器說資源沒有異動,請繼續使用 Cache 的內容。

我就在 /Cache/Scripts 裡面加上一段判斷 If-Modified-Since 的邏輯,當資源的最後修改時間沒變的時候,回傳 304(Not Modified)。

可以看到首次要求資源時,伺服器就會在 Response Header 加上 Last-Modified。

當 Cache 過期瀏覽器向伺服器發送資源請求時,就會加上 If-Modified-Since 的 Request Header,也可以看到畫面上的伺服器時間沒有跳,表示瀏覽器取用的是 Cache 的內容。

ETag 與 If-None-Match

萬一資源有沒有異動用最後修改時間來判斷不準的時候怎麼辦? 可以改用 ETag 配合 If-None-Match 來操作,用法跟 Last-Modified/If-Modified-Since 一樣,ETag 裡面塞的是一個判斷資源有沒有異動的識別值(不一定得是資源內容的 Hash),瀏覽器首次從伺服器發送資源請求時,伺服器就將識別值塞入 ETag 回應,待 Cache 過期的時候,瀏覽器向伺服器發送資源請求時,再把 ETag 值塞入 If-None-Match 發送。

一樣我在 /Cache/Scripts 也來實作這一段邏輯

可以看到首次要求資源的時候,伺服器就將 ETag 回應給瀏覽器。

當 Cache 過期時帶上 If-None-Match 發送請求,伺服器發現識別值沒有異動,回應 304(Not Modified)。

瀏覽器重新整理(F5)對 Cache 的影響

剛剛 Cache 的範例重點放在 HTML 頁面內引用的資源,那麼如果 HTML 頁面資源也加上 Cache-Control: max-age=[n] 是不是也有相同的效果? 我們來試試,我改一下 HTML 內容直接用 Razor 語法印出伺服器的目前時間。

然後為 HTML 頁面資源設置 Cache-Control: max-age=10

我們就會發現沒有屁用,時間一直被更新。

關鍵原因在瀏覽器的重新整理(F5)這件事情,它發送的資源請求中加入了 Cache-Control: max-age=0,什麼意思? 就是告訴瀏覽器說這個資源已經舊了,麻煩瀏覽器跟伺服器發送資源請求。

所以如果是 HTML 頁面的資源,我們應該要搭配 Last-Modified/If-Modified-Since 或 ETag/If-None-Match 來使用,只要最後修改時間或識別值不變,就回應 304(Not Modified)。

以上針對瀏覽器端的 Cache 介紹到這邊,它是能夠提升資源重用率的一個不錯的方式,設計 Web 應用程式應該要去認知到後端資源的成本是比較高的,要儘量透過前端或其他資源來減少對後端資源的依賴。

參考資料

相關資源

C# 指南
ASP.NET 教學
ASP.NET MVC 指引
Azure SQL Database 教學
SQL Server 教學
Xamarin.Forms 教學