包包baobaolin.com
date
entry
019
topic
infrastructure
rev

靜態站要三種快取策略,不是一種

用一條 max-age 涵蓋全站,只有兩種結局:設太長,改了東西線上沒反應;設太短,每個請求都回源,CDN 等於白裝。判準其實只有一句話。

這個檔案的名字,會不會隨著內容改變?

答案是「會」的檔案可以永久快取——因為內容變了它就是另一個網址,舊的那份沒人會再要。答案是「不會」的檔案必須每次確認——因為同一個網址明天可能是不同的內容。

靜態站按這條線切下去正好三類:

類別檔名會變?Cache-Control
帶雜湊的資產(JS/CSS/圖)public, max-age=31536000, immutable
HTML不會public, max-age=0, must-revalidate
feed/sitemap不會public, max-age=3600

三段 sync

aws s3 sync--cache-control 是整批套用的,所以要分三次跑,用 --exclude--include 把檔案切開:

# 1. 帶雜湊的資產:一年,永不重驗
aws s3 sync dist/ "s3://$BUCKET/" --delete \
  --exclude "*.html" --exclude "*.xml" \
  --cache-control "public, max-age=31536000, immutable"

# 2. HTML:可以存,但每次都要問
aws s3 sync dist/ "s3://$BUCKET/" --delete \
  --exclude "*" --include "*.html" \
  --cache-control "public, max-age=0, must-revalidate" \
  --content-type "text/html; charset=utf-8"

# 3. feed 與 sitemap:一小時
aws s3 sync dist/ "s3://$BUCKET/" \
  --exclude "*" --include "*.xml" \
  --cache-control "public, max-age=3600"

三段的過濾規則互斥,所以每個檔案只會被其中一段處理到,不會互相覆寫。順序不影響結果。

immutable 跟 max-age 差在哪

一年的 max-age 已經很長了,那 immutable 還多做什麼?差別在使用者按重新整理的時候

沒有 immutable 時,重新整理會讓瀏覽器對所有資源發出重新驗證請求——就算它們還在有效期內。這些請求多半拿到 304,沒有傳輸資料,但往返時間還是要付immutable 明確告訴瀏覽器:這個網址的內容永遠不會變,連問都不用問。

前提是檔名真的帶雜湊。如果你的 app.js 名字固定、內容會換,套上 immutable 就等於把使用者鎖在舊版一年,而且沒有辦法從伺服器端解除——只能改網址。

max-age=0 不是「不要快取」

這是最常被誤解的一條。max-age=0, must-revalidate 的意思是「可以存下來,但每次使用前都要問一次還新不新」,不是「不准存」。

差別在於配上 ETag 之後,那個「問一次」多半會得到 304 Not Modified:沒有傳輸 HTML 內容,只有一次往返。對一份 14 KB 的頁面,這省下的是絕大部分的位元組。

真的要禁止快取是 no-store,那是給含個人資料的動態頁面用的。靜態站的 HTML 不需要它。

第二段那個 --content-type "text/html; charset=utf-8" 不要省。S3 猜得出 .html 是 HTML,但不會自己補 charset,而 HTTP header 的優先權高於 HTML 裡的 <meta charset>。中文站少了它,在某些瀏覽器設定下就是一頁亂碼。

feed 為什麼是一小時

feed 和 sitemap 的檔名固定,理論上該跟 HTML 一樣每次重驗。我給它一小時,理由是讀者不需要秒級的新鮮度——RSS 閱讀器本來就是幾十分鐘輪詢一次,搜尋引擎抓 sitemap 更慢。

一小時的代價是:發文之後,最多一小時內訂閱者才會看到。這對一個部落格是可以接受的;如果是新聞站就不行。快取時間該由「多晚看到會有問題」決定,不是由檔案類型決定。

invalidation 不是快取策略

CloudFront 的 invalidation 很好用,但它是逃生口,不是設計的一部分。它有三個成本:要錢(超過免費額度之後)、要時間(幾十秒到幾分鐘)、而且它清的是 CDN,清不掉使用者瀏覽器裡的那份

最後一點是關鍵。如果 HTML 被設成快取一天,那麼即使你 invalidate 了 CDN,昨天來過的使用者今天看到的還是舊頁面,而你在自己的無痕視窗裡完全看不到這個問題。

所以 HTML 那條 max-age=0 才是主角,invalidation 只是讓 CDN 這一層立刻跟上。

也因為 invalidation 有延遲,部署腳本在比對全站內容之前要先等一段時間——太早驗證會讀到還沒失效的邊緣節點,得到假的失敗。

如果只記得一件事

快取策略是由檔名的性質決定的,不是由你希望它多快更新決定的。 檔名帶雜湊就快取到永遠,檔名固定就每次確認。中間那些「設個十分鐘應該還好」的直覺數字,通常同時拿到兩邊的缺點。

修訂紀錄

  1. 首次發布