跳轉到

8.2 快取策略精修#

本節目標#

  • 掌握 Cache-Control 語意與 stale-while-revalidate 的時間數學。
  • 會在 Worker 內使用 Cache API(caches.default),與 CDN 邊緣快取分工。
  • 使用 Cache Rules 與 Purge Cache(手動與 API)管理失效。
  • 理解邊緣快取底層的 Bypass / Respect origin 兩種行為模式差異。

快取的經濟學回顧#

1.1 節說過:CDN 的價值來自「多數請求不回源」。量化它:

\[ \text{回源率} = 1 - \text{命中率} \quad\Rightarrow\quad \text{TTFB}_{\text{均}} = h \cdot t_{\text{hit}} + (1-h)(t_{\text{miss}}) \]

命中率 \(h\) 從 0.85 提升到 0.95,看似只差 10%,但「未命中」才是延遲與源站壓力的大頭——因為未命中往往伴隨驚群效應(thundering herd):同一 URL 同時湧入 50 個查詢,全部穿透回源。這就是本章要「精修」的原因。

Cache-Control:標準語意一覽#

Cache-Control: max-age=300, s-maxage=86400, stale-while-revalidate=600, stale-if-error=86400
指令 對象 意義
max-age 瀏覽器 客戶端快取多久
s-maxage 共享快取(CDN) 覆蓋 max-age(CDN 可存更久)
stale-while-revalidate 兩者 過期後可直接回舊的,同時背景驗證
stale-if-error 兩者 源站掛掉時可繼續回舊資料

stale-while-revalidate 的時間線#

這是最值得投資的一行設定:使用者永遠拿到即時響應(哪怕稍舊),新鮮度在「無感」中完成。

 t=0     ─→ 回應新鮮(max-age 內)
 t=300   ─→ 過期。第 1 個請求回 stale 舊檔 + 背後觸發 revalidate
 t=302   ─→ 快取已是新版;600 秒內的後續請求都瞬間回應
 t=1500  ─→ SWR 窗口都過了 → 才真正同步回源
\[ \text{使用者感知延遲} \approx \begin{cases} t_{\text{hit}} & \text{max-age 內或 SWR 窗口內} \\ t_{\text{miss}} & \text{全過期(少數)}\end{cases} \]

靜態資源(JS/CSS 帶 hash)可以放心 max-age=31536000, immutable,因為檔名即版本,失效問題被 URL 命名解決——這是工程上最便宜的失效策略。

三層快取架構:瀏覽器、邊緣、Worker 內#

前面章節部署的 Workers 屬於「動態程式」,但它自己也能用 Cache API 再快取一層(例如上游 API 的 JSON):

export default {
  async fetch(req: Request, env: Env, ctx: ExecutionContext) {
    const url = new URL(req.url);
    const cache = caches.default;
    let res = await cache.match(req);

    if (!res) {
      // cache miss:回源(假設是 D1 查詢組裝的結果)
      res = await buildResponse(url);          // 動態產生
      res = new Response(res.body, res);       // Response 可能不可變,複製一份
      res.headers.set("Cache-Control", "s-maxage=300, stale-while-revalidate=600");
      // 注意:cache.put 只在回源執行緒做,且 206/串流有部分限制
      ctx.waitUntil(cache.put(req, res.clone()));
      res.headers.set("X-Cache", "MISS");
    } else {
      res = new Response(res.body, res);
      res.headers.set("X-Cache", "HIT");       // 選擇性回報給自己觀測(8.1)
    }
    return res;
  }
} satisfies ExportedHandler<Env>;

三層的分工原則:

層 誰控制 典型 TTL
瀏覽器快取 max-age / 服務 Worker 靜態資源長、動態 API 用 no-store
邊緣 CDN 快取 s-maxage + Cache Rules 頁面/媒體數分鐘~數月
Worker 內 Cache API 你的程式碼 上游資料的微小副本

Cache Rules:宣告式控制#

希望你不必寫程式就能影響 CDN 層的快取行為:Dashboard → Caching → Cache Rules(2022 起取代 Page Rules)。

範例規則:hostname eq cdn.example.com and path matches "^/thumb/.*" →

  • Cache Eligibility:Eligible for cache
  • Edge TTL:Override origin,設 7 天
  • Browser TTL:Respect origin
  • Query String:忽略所有 query 參數(圖片縮圖很適合)

Bypass Cache vs Respect origin:同一個規則的兩種靈魂#

Cache Rules / 底層快取對「要不要遵守源站 Cache-Control」的態度不同:

模式 作用 使用時機
Bypass cache 該請求完全不進快取,每次回源(也不寫快取) API、帶身分 Cookie 的動態頁
Respect origin(預設) 看源站怎麼說:Cache-Control: no-store 就不存;有 s-maxage 就以它為準 你能控制源站 header 時(推荐,職責單一)
Override origin 優先於源站 header 源站你動不了(第三方平台)時

問題的本質:快取決策要放在「知道資料新鮮度語意的程式碼」還是「基礎設施的設定」?經驗法則:能用 origin header 控制(Respect origin)就用它,讓 Worker 的 header 設定成為唯一真相;只有「源站不受你控」時才用 override。

Purge Cache:失效的兩條路#

1.6 節介紹過 Scheduled Cache 的概念,實務上你有兩個入口:

# 手動(Dashboard 或本例的 API)
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
  -H "Authorization: Bearer $CF_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"purge_everything": true}'

# 指定檔案(更快,影響面小)
curl -X POST ".../purge_cache" -H "Authorization: Bearer $CF_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"files": ["https://example.com/index.html", "https://example.com/style.css"]}'

重要觀念:

  • Cache API(Worker 內)與 CDN 邊緣快取是兩套不同的儲存。Purge 只清 CDN 層;你若 cache.put 過,要用 cache.match + 不回存(或檔案層級控制)自己管理——這是初學最常踩的坑。
  • purge_everything 生效在數秒內,但全站會瞬間變成 100% miss,回源壓力突然放大,建議挑離峰時段執行。

小結與練習#

重點回顧

  1. 命中率的小幅提升可以大幅降低未命中的回源成本;SWR 讓過期帶來「零延遲退路」。
  2. 三層快取各司其職:瀏覽器(帶 hash 的資源存久)、邊緣 CDN(s-maxage + Cache Rules)、Worker 內 Cache API(獨立儲存,要自己管失效)。
  3. 快取失效有兩種模式:URL 命名(最便宜)與 Purge(必要時)。
  4. Bypass = 完全不碰快取,Respect origin = 源站 header 說了算。

練習

  1. 給一個由 Worker 提供的 HTML 頁與一個帶 hash 的 JS 檔,設計各自的 Cache-Control,並解釋為什麼 HTML 不適合長 max-age。
  2. 在 Worker 中實作 cache.put 用正規化 URL(去掉不影響內容的 query)當 key,量測命中率提升。
  3. 試著推一次 purge_everything,並觀察接下來 10 分鐘內 8.1 節 Observability 的 CPU 曲線變化,說明為什麼 purge 會造成算力尖峰。