banner
约 2,100 字
7 分钟

从 Cache API 到 Workers Cache:命中不再消耗 CPU Time

摘要

Cloudflare Workers Cache 将缓存层置于 Worker 之前,命中时完全不启动 Worker,CPU Time 直接归零。本文深入解析其工作机制、并发合并与分层缓存优势、配置防踩坑指南,以及本博客采用双 Entrypoint 架构的具体落地实践。

之前在《Cloudflare Workers 全栈应用:正确使用 Cache API 加速网站》里聊过:在 Cloudflare Workers 上,如果想在边缘节点缓存 SSR 渲染的 HTML,光靠在响应头里加 Cache-Control 是不起作用的,必须自己在 Worker 里调用 caches.default.matchput 手动存取。

这套方案确实能把页面缓存在全球边缘,命中时也能跳过耗时的模板渲染和查库。但它有一个挥之不去的痛点:无论请求是否命中缓存,每次都得完整走一遍 Worker 的调用链路——启动或唤醒 isolate、执行 fetch 入口、再做一次异步 cache.match。只要代码跑了,后台就会产生 CPU Time 计费。

后来 Cloudflare 官方推出了 Workers Cache。它把缓存层直接提到了 Worker 的最前端:只要边缘缓存命中,请求直接由 CDN 吐出响应,后端的 Worker 完全不启动,这段时间的 CPU Time 直接归零。

这篇文章就来聊聊 Workers Cache 的核心机制、相比 Cache API 的优势、具体配置方法,以及本博客在实际迁移过程中踩过的坑与落地方案。

1. 核心机制:它到底是什么?

Workers Cache 是绑定在当前 Worker 上的独立边缘缓存,与你在 Cloudflare 控制台里配置的 Zone 级 Cache Rules、Page Rules 完全无关。

当你在 wrangler.jsonc 中开启 cache.enabled 之后,Cloudflare 的请求调度管道会发生根本改变:

  • 请求到达边缘:Cloudflare 在真正调用你的 Worker 代码之前,会先去内部缓存区查找该请求。

  • 缓存命中(Cache Hit):直接将缓存的 Response 返回给客户端,Worker 代码完全不执行

  • 缓存未命中(Cache Miss):调用你的 Worker 执行业务逻辑,并在收到响应后,根据你返回的 Cache-Control 标头决定是否写回缓存。

这与 Cache API 的差别就在于一前一后:Cache API 是你在 Worker 的 fetch 内部手动读写;而 Workers Cache 是平台在进入 Worker 之前就提前拦下一道。对于新建的全栈项目,官方也强烈建议优先采用 Workers Cache。

2. 核心优势:不仅是省 CPU

最直接的收益当然是降低账单与计算开销。Cloudflare 官方文档写得很明确:“CPU time is only billed when your Worker runs.” 对于像个人博客、文档站这种“同一 URL 大量重复访问”的读密集型 SSR 场景,大部分请求的 CPU 耗时都会直接降为 0。

除此之外,它还原生自带了两个 Cache API 无法简单实现的特性:

  1. 请求并发合并(Request Collapsing / Single Flight):当某个页面的缓存刚过期,瞬间涌入数十个并发请求时,Workers Cache 会自动将这些相同 Cache Key 的请求收敛,只放一个请求去跑 Worker,其他并发请求在边缘排队等待这次渲染的结果,天然杜绝了缓存击穿打爆后端数据库的问题。

  2. 默认开启 Tiered Cache(分层缓存):某一个区域的上层数据中心生成缓存后,周边其他地区的节点也能直接复用,而不需要全球每个边缘机房都各自冷启动 Miss 一次。

  3. 更轻量无感的 Tag Purge:以前通过 API 清除缓存,必须去配置全局 API Key 或申请带有 Zone.Cache:Purge 权限的 Token;而 Workers Cache 支持直接在代码里调用 ctx.cache.purge({ tags: ["posts"] }),作用域天然隔离在当前 Worker 内部,无需任何外部网络请求与凭据配置。

3. 实战配置指南

在常规场景下,接入 Workers Cache 仅需配置与响应头配合。

开启缓存

wrangler.jsonc(或 wrangler.toml)中开启功能:

JSON
{
  "name": "my-worker",
  "main": "src/index.ts",
  "compatibility_date": "2026-09-12",
  "cache": {
    "enabled": true
  }
}

如果整个 Worker 处理的都是可缓存的内容,配置这一处即可。

返回可缓存的响应头

TypeScript
return new Response(html, {
  headers: {
    // 指导浏览器:每次访问都向服务器进行新鲜度验证
    "Cache-Control": "public, max-age=0, must-revalidate",
    // 指导 Cloudflare 边缘:缓存 1 小时,且支持 24 小时内的后台异步刷新
    "CDN-Cache-Control": "public, max-age=3600, stale-while-revalidate=86400",
    // 注入业务标签,方便后续按标签精准失效
    "Cache-Tag": "html,posts",
  },
});

Workers Cache 支持标准的 Cache-Control,也支持优先级更高的 CDN-Cache-ControlCloudflare-CDN-Cache-Control

这里有一个非常容易踩的坑must-revalidates-maxage 会直接关闭 stale-while-revalidate(SWR)机制。如果直接在普通的 Cache-Control 上写 public, max-age=0, must-revalidate,边缘节点过期后也会每次强行回源并阻断用户。

正确的做法是分层配置:将 Cache-Control 留给客户端浏览器,边缘专属的 TTL 和 SWR 策略则写在 CDN-Cache-Control 中。这样页面在边缘过期后,依然能瞬间向访客返回旧页面,并在后台异步回源刷新,体验丝滑。

此外,Cache-Tag 标头会被 Cloudflare 自动剥离,不会泄露给外部客户端。

验证命中状态

连续发起两次请求,观察响应标头中的 cf-cache-status

  • 第一次:MISS(触发 Worker 渲染并写入缓存)

  • 第二次:HIT(Worker 不执行,毫秒级直接响应)

主动失效(Purge)

TypeScript
await ctx.cache.purge({ tags: ["posts"] });

注意:purge 只对写入缓存的那个入口点(Entrypoint)生效。

4. 本博客的落地实践:双 Entrypoint 架构

在真实的生产项目中,事情往往没那么简单。以本博客为例,我们遇到了一个两难问题:

  • 如果对整个 Worker 简单粗暴地开启缓存,那么在进入 SSR 渲染之前,用来处理 Cookie、解析语言(i18n)、甚至重定向的逻辑就会全部被缓存拦截;

  • 如果让前置逻辑满世界返回 no-store,请求每次依然会先做一次无意义的缓存查找,平白增加几十毫秒的链路延迟。

为了解决这个问题,我们在 Wrangler 中采用了多 Entrypoint(网关分发 + 核心应用)的拆分架构:

JSON
{
  "cache": {
    "enabled": true
  },
  "exports": {
    "default": { "type": "worker", "cache": { "enabled": false } },
    "App": { "type": "worker", "cache": { "enabled": true } }
  }
}
TypeScript
export default {
  async fetch(request, _env, ctx) {
    // 1. 在不缓存的 default 入口层轻量提取语言上下文
    const locale = extractLocaleFromRequest(request);

    // 2. 携带 props 转发给启用了缓存的 App 入口
    return ctx.exports.App({ props: { locale } }).fetch(request, {
      cf: { cacheKey: workersCacheKey(request.url) },
    });
  },
};

在这套架构下:

  • `default` 入口(关闭缓存):充当轻量 API Gateway,仅负责解析请求语言、注入 ctx.props,几乎不消耗 CPU Time。

  • `App` 入口(开启缓存):负责真正的 SSR 渲染。缓存命中时,App 完全不执行;未命中时才做实际渲染并打上 Cache-Tag

  • 多语言隔离:中英文版本通过 ctx.props 生成不同的 Cache Key,无需在公共 URL 路径中强制加入语言前缀,也能完美独立缓存。

  • 强一致性更新:在发布新文章或更改站点配置时,后台服务会主动触发 ctx.cache.purge({ tags: ["posts"] }),确保“文章一更新,全网秒生效”。

对于管理后台、登录授权、动态 API 等路由,统一返回 private, no-store,确保私有敏感数据绝不进入公有缓存。

5. 计费与使用注意事项

  • 计费模式:Workers Cache 本身不收取单独的功能费用。缓存命中时依然会作为一次常规的 Worker Request 计费,但计费项中的 CPU Duration 为零

  • 计费范围变动:在开启缓存后,原本由静态资源托管处理的请求以及 Worker 之间的调用,若经过该缓存层,也会按标准请求计入请求次数。对于计算密集、读多写少的 SSR 博客而言,节省下来的 CPU 开销依然非常可观。

END