Appearance
Cache-Control
是用來設定資源在瀏覽器上的快取行為的標頭,這在網路上已經有太多的文章介紹了,在此不贅述,本篇文章想記錄的是最近偶然發現的一個瀏覽器的快取行為 - Heuristic Cache
目前沒看過有中文翻譯,照字面上翻為「啟發式快取」?
事情的起因是我觀察到同樣都沒有 Cache-Control
標頭的資源,為什麼有些會被瀏覽器快取,有些則不會? 這讓我好奇瀏覽器是怎麼決定這些沒有 Cache-Control
標頭的資源是如何被快取的? 快取時間為多久?
一開始我比較兩個資源標頭的差異,都沒看出一點端倪,因為它們的標頭都是一樣的,直到我在 stackoverflow 看到一則回覆,裡面似乎提到了一種算法規則,才知道原來魔鬼藏在 Date
與 Last-Modified
這兩個標頭裡,進一步搜尋相關文件後,發現了 Heuristic Cache 這個從未見過的名詞,裡面解釋道:
HTTP is designed to cache as much as possible, so even if no Cache-Control is given, responses will get stored and reused if certain conditions are met. This is called heuristic caching.
意即即使沒有 Cache-Control
標頭,只要符合某些條件,瀏覽器也會對資源做快取,這就是所謂的啟發式快取。
It is heuristically known that content which has not been updated for a full year will not be updated for some time after that. Therefore, the client stores this response (despite the lack of max-age) and reuses it for a while. How long to reuse is up to the implementation, but the specification recommends about 10% of the time after storing.
這段內容大概是說如果一個資源在很長一段時間沒有更新,那麼大概也可以假設它在之後的一段時間也不會更新。 因此瀏覽器會將這類「未來大概也不會更新」的資源做快取,快取時間有多久是由瀏覽器實作決定。 關於瀏覽器的實作細節 Chrome 的部分可以參考 Chromium 實作 Heuristic Cache 的源碼。
那麼瀏覽器怎麼知道資源多久沒有更新呢?這就是依賴 Date
與 Last-Modified
兩個標頭:
Date
是伺服器回應的時間,通常是當下的時間,後文會說另一種可能。Last-Modified
是資源的最後修改時間(Last-Modified
與ETag
之間的關係本篇不談)。
MDN 文件沒有明確說明 Heuristic Cache 的運作機制,因此我找了另一篇講得比較完整的文章[1],讀完模擬了不同 HTTP response header 的實驗,在 Chrome 瀏覽器得到以下結論:
當一個資源沒有 Cache-Control
標頭時,瀏覽器使用 Date
及 Last-Modified
標頭先計算出一個時間長度,公式為 時間長度 = (Date - Last-Modified) / 10
單位是秒;快取過期時間點的公式為 快取過期的時間 = Date + 時間長度
。
INFO
如果缺少 Date
標頭,或值不符合 RFC 1123 時間格式[2]規範導致值無法正確被解析,則瀏覽器會使用當下的時間作為 Date
。
舉例來說:
http
date: Fri, 16 Aug 2024 09:25:00 GMT
last-modified: Fri, 16 Aug 2024 09:08:20 GMT
# 時間長度 = (8/16 09:25:00 - 8/16 09:08:20) / 10 = 100s
# 快取過期的時間點 = 8/16 09:25:00 + 100s = 8/16 09:26:40
以上面這個例子來說,假設在快取過期時間點
之前第一次請求資源,瀏覽器就會將它快取,後續請求只要在快取過期時間點
之前,都是直接取用 Disk 的快取,不會再向伺服器發送請求。
值得一提的是只要格式正確 Date
可以是過去的時間,也可以是未來的時間,瀏覽器是否將資源快取下來的規則是固定的,符合 當下時間 < 快取過期的時間點
就做快取。 也因為 Date
不一定等於 now, 所以我認為這種快取機制用「過期時間點」來理解會好過於用「多久以後過期」。
什麼情況 Date
不等於 now 呢?
使用到 CDN 服務像是 Cloudfront、Cloudflare,舉例一種可能:
- 使用者向 CDN 發送請求。
- CDN 向原始伺服器發送請求。
- 原始伺服器回應請求給 CDN 包含
Date
標頭。 - CDN 收到原始伺服器回應,將回應連投標頭快取下來,並回應給使用者。
- 一段時間後,使用者(不一定是同一個)再次向 CDN 發送相同資源的請求,CDN 快取沒過期,直接用先前的快取回應給使用者。
- 使用者收到的
Date
標頭是先前的時間。
這種情況 Date
反映出的是 CDN 快取時間點,而不是當下即時的時間。
這會衍生問題,假設有一個網站上線後不再更新(資源的 Last-Modified
就不會改變),且 CDN 快取永不過期,導致 Date
因為 CDN 關係也被鎖在一個時間點,兩個變數固定後所產生的 快取過期時間點
也是固定的一個時間點,而且這個時間點基本上跟鎖死的 Date 不會差太遠,幾乎可以說 now 恆大於 快取過期時間點
,這種情況下本文提到的快取機制幾乎不會起作用,每次都會向 CDN 發出請求。
所以說 Heuristic Cache
可能會因為使用 CDN 關係而失靈或不如預期,一種方法是顯性聲明資源的 Cache-Control
換言之就是不要有 Heuristic Cache
,不要讓瀏覽器幫你決定快取行為; 又或者是在 CDN 服務上找到對應的設定來解決,以 Cloudfront 來說可以使用 Response headers policy
底下的 Remove Headers
來移除 Date
標頭,官方文件[3]特別解釋這部分,當你移除掉 Date
移除的是原始伺服器回應的 Date
,Cloudfront 會添加自己的 Date
回應給使用者,此時的 Date
就是動態的 now 而不是快取的時間,當 Date
會隨時間與 Last-Modified
產生差距的時候,Heuristic Cache
的機制就會發揮作用。
什麼? Age header 竟然也會影響快取時間
實驗得出結論後沒隔幾天,又發現了一個新狀況,假設一個資源的 Date
與 Last-Modified
都是在一天後的某個相當接近的時間,照前面得出的公式來說,瀏覽器應該會快取此資源直到隔天,但實際上卻是每次都發出新的資源請求。
最後發現是 Age
header 搞得鬼,這個 Header 通常是用來記錄快取伺服器與原始伺服器之間資源存活的時間,例如:某個資源在 Cloudfront 快取伺服器上存活了 1 小時,Age 就會是 3600。 但竟沒想到還會對 Heuristic Cache
產生影響。
網路上找不關於 Age
如何影響 Heuristic Cache 快取時間的文章,我做了一些 edge case 實驗,得到以下結論:
沒有 Age 標頭,計算 Heuristic Cache 的過期時間就同本文前面所述; 如果 Age 標頭存在,則 Heuristic Cache 的過期時間如下:
效期(秒) = (Date - Last-Modified) / 10 - Age
快取過期時間點 = Now + 效期(秒)
注意這裡是 Now 而不是 Date。條件時間點 = Date + (Date - Last-Modified) / 10
條件 = Now < 條件時間點
如果條件不成立,則不會快取。
舉例來說:
http
Age: 80
date: Sun, 25 Aug 2024 09:25:00 GMT
last-modified: Sun, 25 Aug 2024 09:08:20 GMT
# 效期 = (Date - Last-Modified) / 10 - Age = 20s
# 條件時間點 = Date + (Date - Last-Modified) / 10 = Date + 100s = 09:26:40
# 如果 Client 端的時間在 09:26:40 之前,則快取時間過期時間為 now + 20s
# 如果 Client 端的時間在 09:26:40 之後,則不做快取
# 假設此時此刻為 09:26:00,則快取過期時間點為 09:26:20
# 假設此時此刻為 09:22:00,則快取過期時間點為 09:22:20
# 假設此時此刻為 09:26:50,則不做快取
補充 - Cache-Control - max-age
在本篇文章實驗中也得出了 Cache-Control max-age 與 Date
標頭一些有趣關係:
Date
是 1 小時前,max-age
為 3600,不會產生快取,因為 -3600 + 3600 = 0。Date
是 1 小時前,max-age
為 3610,會產生維持 10 秒的快取。- 但如果
Date
是 1 小時後的未來時間,max-age
為 10,快取時間不會是 3600 + 10 而是 10 秒。
總結
Heuristic Cache
發生在沒有 Cache-Control
標頭的情況,根據 Date
與 Last-Modified
標頭來決定資源快取在瀏覽器的時間,同時 Age
標頭也扮演著影響快取時間及快取條件的角色。
Date
標頭可以是過去也可以是未來的時間,通常是伺服器的回應時間,如果 Date
標頭總是為 now 則 Heuristic Cache
的快取時間可以簡化為 (Date - Last-Modified) / 10
兩時間差的 10% ;如果 Date 是一個非 now 的時間,則用 Date + (Date - Last-Modified) / 10
來算出「什麼時候到期」會比較好理解。
Date
有可能會因為使用邊緣快取服務導致此標頭鎖在過去的時間點,進而影響 Heuristic Cache
的運作,甚至讓 Cache-Control
失效,所以確保在使用 CDN 相關服務時做好設定,使 Date
是動態的 now 。
Age
標頭對於 Heuristic Cache
也具有影響,如果 Age
存在,則快取的效期會是 Now + (Date - Last-Modified) / 10 - Age
,前提是 Now < Date + (Date - Last-Modified) / 10
條件成立,否則不會快取。
Heuristic Cache
這個機制在瀏覽器上是隱藏的,但對於開發者來說是一個值得注意的地方,有助於了解瀏覽器背後的一堆資源請求為什麼有些是從 disk 快取得; 如果對於此機制沒有安全感,最好還是使用 Cache-Control
來明確定義資源的快取行為。