Skip to content

HTTP Heuristic Cache

Cache-Control 是用來設定資源在瀏覽器上的快取行為的標頭,這在網路上已經有太多的文章介紹了,在此不贅述,本篇文章想記錄的是最近偶然發現的一個瀏覽器的快取行為 - Heuristic Cache 目前沒看過有中文翻譯,照字面上翻為「啟發式快取」?

事情的起因是我觀察到同樣都沒有 Cache-Control 標頭的資源,為什麼有些會被瀏覽器快取,有些則不會? 這讓我好奇瀏覽器是怎麼決定這些沒有 Cache-Control 標頭的資源是如何被快取的? 快取時間為多久?

一開始我比較兩個資源標頭的差異,都沒看出一點端倪,因為它們的標頭都是一樣的,直到我在 stackoverflow 看到一則回覆,裡面似乎提到了一種算法規則,才知道原來魔鬼藏在 DateLast-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 的源碼

那麼瀏覽器怎麼知道資源多久沒有更新呢?這就是依賴 DateLast-Modified 兩個標頭:

  • Date 是伺服器回應的時間,通常是當下的時間,後文會說另一種可能。
  • Last-Modified 是資源的最後修改時間(Last-ModifiedETag 之間的關係本篇不談)。

MDN 文件沒有明確說明 Heuristic Cache 的運作機制,因此我找了另一篇講得比較完整的文章[1],讀完模擬了不同 HTTP response header 的實驗,在 Chrome 瀏覽器得到以下結論:

當一個資源沒有 Cache-Control 標頭時,瀏覽器使用 DateLast-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,舉例一種可能:

  1. 使用者向 CDN 發送請求。
  2. CDN 向原始伺服器發送請求。
  3. 原始伺服器回應請求給 CDN 包含 Date 標頭。
  4. CDN 收到原始伺服器回應,將回應連投標頭快取下來,並回應給使用者。
  5. 一段時間後,使用者(不一定是同一個)再次向 CDN 發送相同資源的請求,CDN 快取沒過期,直接用先前的快取回應給使用者。
  6. 使用者收到的 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 竟然也會影響快取時間

實驗得出結論後沒隔幾天,又發現了一個新狀況,假設一個資源的 DateLast-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 標頭一些有趣關係:

  1. Date 是 1 小時前, max-age 為 3600,不會產生快取,因為 -3600 + 3600 = 0。
  2. Date 是 1 小時前, max-age 為 3610,會產生維持 10 秒的快取。
  3. 但如果 Date 是 1 小時後的未來時間,max-age 為 10,快取時間不會是 3600 + 10 而是 10 秒。

總結

Heuristic Cache 發生在沒有 Cache-Control 標頭的情況,根據 DateLast-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 來明確定義資源的快取行為。


  1. How long does the heuristic cache of the browser actually cache? ↩︎

  2. RFC 1123 Time Format ↩︎

  3. Understand response headers policies - Remove headers ↩︎

Written By
YI FENG XIE
YI FENG XIE

Creative Problem Solver