perf: 开启 Workers Cache 缓存 favicon,并给动态接口兜底 no-store
背景:favicon 此前只有 Cache-Control: max-age(仅浏览器缓存),
CF 边缘不缓存 Worker 响应 → 每次请求都进 Worker → 第一句就是 KV.get。
友圈页按友链数放大,KV 读量被显著放大。
★ 关键纠错:zone 级 Cache Rules 对 Worker 响应**完全无效**。
原因是 Worker 位于 zone 缓存之前("Workers sits in front of cache"),
且 Worker 直接生成响应时不发 fetch 子请求,cache status 只能是 none/unknown。
查证 CF 官方文档后确认,正确方案是 [cache] enabled = true(Workers Cache,
2026-07 上线,需 wrangler >= 4.69)。
改动:
- wrangler.toml: 新增 [cache] enabled = true;compatibility_date 提到 2026-07-24
- package.json: wrangler ^3.99 → ^4.147,workers-types ^4 → ^5(peer 要求)
- index.ts: 抽出 dispatchRequest;新增 ensureCachePolicy 兜底
★ 这一步是必须的:Workers Cache 遵循 RFC 9111 含**启发式缓存**,
没带 Cache-Control 的响应也可能被缓存。实测 /api/v2/comments 开启后
立刻被缓存(HIT, age:29)→ 用户发的新评论看不见。
现在白名单式兜底:已显式声明可缓存的(favicon 的 s-maxage)保留,
其余一律补 no-store。
验证(中转机实测,已清空 zone 的无效 cache rule):
- /api/favicon → MISS → HIT,age 递增(Worker 不再执行,KV 读归零)
- /api/v2/comments → BYPASS ×3,cache-control: no-store
- /api/v2/conf → BYPASS,no-store
- /api/v2/healthz → BYPASS,no-store
This commit is contained in:
1 parent
d2f1d176af
commit
ca2fb7163d
3 files changed
+91
-26
No files matched your search
@@ -14,8 +14,8 @@
|
||||
"tail": "wrangler tail"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20250109.0",
|
||||
"@cloudflare/workers-types": "^5.20261006.1",
|
||||
"typescript": "^5.7.0",
|
||||
"wrangler": "^3.99.0"
|
||||
"wrangler": "^4.147.0"
|
||||
}
|
||||
}
|
||||
+71
-23
@@ -122,34 +122,82 @@ router.get('/transfer/export', A.transferExport);
|
||||
router.post('/transfer/import', A.transferImport);
|
||||
router.post('/transfer/upload', A.transferUpload);
|
||||
|
||||
// ---------------------------------------------------------------- 请求分发
|
||||
|
||||
async function dispatchRequest(
|
||||
req: Request,
|
||||
env: Env,
|
||||
ctx: ExecutionContext,
|
||||
url: URL,
|
||||
): Promise<Response> {
|
||||
// R2 里的图片:/file/** 或 /api/v2/file/**
|
||||
if (req.method === 'GET' && /^(\/api\/v2)?\/file\//.test(url.pathname)) {
|
||||
return A.serveFile({
|
||||
env,
|
||||
req,
|
||||
url,
|
||||
params: {},
|
||||
user: null,
|
||||
});
|
||||
}
|
||||
|
||||
// RSS 订阅模块(rss-robot 合并进来):/api/*(Artalk 用 /api/v2/* 不会撞)
|
||||
// + /feed 订阅源管理页 + 微信验证文件
|
||||
const p = url.pathname;
|
||||
if (
|
||||
(p.startsWith('/api/') && !p.startsWith('/api/v2')) ||
|
||||
p === '/feed' ||
|
||||
p.startsWith('/MP_verify_')
|
||||
) {
|
||||
return RssApi.fetch(req, env, ctx);
|
||||
}
|
||||
|
||||
return router.dispatch(req, env, ctx);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 缓存策略兜底
|
||||
//
|
||||
// ★ 为什么需要这个:Worker 开启了 Workers Cache(wrangler.toml 的 [cache]
|
||||
// enabled = true)之后,Cloudflare 会在**调用 Worker 之前**先查缓存。
|
||||
// 而 Workers Cache 遵循 RFC 9111 —— 包括**启发式缓存**:一个完全不带
|
||||
// Cache-Control 的响应也可能被判为可缓存!
|
||||
//
|
||||
// 实测教训:`/api/v2/comments` 本来就没设 Cache-Control,开启 Workers Cache
|
||||
// 后立刻被缓存(cf-cache-status: HIT, age: 29)→ 用户发的新评论看不见。
|
||||
//
|
||||
// 规则(白名单式,只放行明确声明可缓存的):
|
||||
// - 上游已带任意 Cache-Control / CDN-Cache-Control → 原样保留(如 favicon 的
|
||||
// s-maxage=86400,它本来就该缓存)
|
||||
// - 其余一律补 `Cache-Control: no-store`(评论、登录、管理接口等禁止缓存)
|
||||
//
|
||||
// ★ 必须是白名单思路:漏掉一个接口 = 数据陈旧 bug;而多补 no-store 只损失一点性能。
|
||||
function ensureCachePolicy(res: Response, req: Request): Response {
|
||||
// 只处理 GET/HEAD(Workers Cache 本就只缓存这两种;其余原样返回)
|
||||
if (req.method !== 'GET' && req.method !== 'HEAD') return res;
|
||||
|
||||
const hasExplicitPolicy =
|
||||
res.headers.has('Cache-Control') ||
|
||||
res.headers.has('CDN-Cache-Control') ||
|
||||
res.headers.has('Cloudflare-CDN-Cache-Control');
|
||||
if (hasExplicitPolicy) return res;
|
||||
|
||||
// 其余(含 3xx / 5xx)一律禁止缓存:宁可少缓存,不可把数据或错误固化
|
||||
const headers = new Headers(res.headers);
|
||||
headers.set('Cache-Control', 'no-store');
|
||||
return new Response(res.body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||
const url = new URL(req.url);
|
||||
|
||||
try {
|
||||
// R2 里的图片:/file/** 或 /api/v2/file/**
|
||||
if (req.method === 'GET' && /^(\/api\/v2)?\/file\//.test(url.pathname)) {
|
||||
return A.serveFile({
|
||||
env,
|
||||
req,
|
||||
url,
|
||||
params: {},
|
||||
user: null,
|
||||
});
|
||||
}
|
||||
|
||||
// RSS 订阅模块(rss-robot 合并进来):/api/*(Artalk 用 /api/v2/* 不会撞)
|
||||
// + /feed 订阅源管理页 + 微信验证文件
|
||||
const p = url.pathname;
|
||||
if (
|
||||
(p.startsWith('/api/') && !p.startsWith('/api/v2')) ||
|
||||
p === '/feed' ||
|
||||
p.startsWith('/MP_verify_')
|
||||
) {
|
||||
return RssApi.fetch(req, env, ctx);
|
||||
}
|
||||
|
||||
return router.dispatch(req, env, ctx);
|
||||
const res = await dispatchRequest(req, env, ctx, url);
|
||||
return ensureCachePolicy(res, req);
|
||||
} catch (e) {
|
||||
// 兜底:任何漏网的异常也必须带 CORS 头,否则浏览器把它当"网络故障",
|
||||
// 读者只能看到 "TypeError: Failed to fetch",根本不知道发生了什么。
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
name = "artalk-cf"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2025-06-01"
|
||||
compatibility_date = "2026-07-24"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ★ Workers Cache —— 缓存 Worker 自己生成的响应(无需回源 fetch)
|
||||
#
|
||||
# 为什么必须开:Worker 位于 zone 缓存之前("Workers sits in front of cache"),
|
||||
# 所以 **zone 级 Cache Rules / Page Rules 对 Worker 响应完全无效**。
|
||||
# 想让 /api/favicon 这类「Worker 直接返回」的响应被边缘缓存、从而不再执行
|
||||
# Worker(省 KV 读 + 省 Worker 请求),唯一正解是这个 [cache] enabled。
|
||||
#
|
||||
# 生效条件(缺一不可):
|
||||
# 1. 这里是 fetch 处理器(scheduled 等不参与缓存)
|
||||
# 2. 响应带可缓存的 Cache-Control(本项目已带 s-maxage=86400)
|
||||
# 3. 只缓存 GET/HEAD;带 Set-Cookie 的响应自动绕过
|
||||
# 验证:连续两次 curl -I,应依次看到 cf-cache-status: MISS → HIT
|
||||
# ---------------------------------------------------------------------------
|
||||
[cache]
|
||||
enabled = true
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# D1 (必须) 创建:npx wrangler d1 create artalk-cf
|
||||
|
||||
Reference in new issue
Block a user