Cloudflare Pages 缓存命中率 7%?我用 3 条规则把它干到 90%
站点上线后国内访问慢成狗,LCP P75 高达 20 秒。 排查后发现 Cloudflare 缓存命中率只有 7.62%。 用 3 条 Cache Rules + 1 个本地化优化,命中率飙到 90%+,首屏从 20s 降到 3s。
问题发现
📚 本文是 UtlKit 技术系列第 9 篇 — 系列索引 →
- 上一篇:PageSpeed CI →
- 下一篇:[暗色模式 →]
站点 utlkit.com 部署到 Cloudflare Pages 后,在国内访问非常慢。打开 Chrome DevTools 一看,LCP P75 高达 20 秒,P90 26 秒。
第一反应是"Cloudflare 在国内不是减速器吗",去后台看了 Cache Analytics——
缓存命中率 7.62%。
也就是说 92% 的请求都没有命中缓存,全部回源。Cloudflare Pages 的源站是边缘节点本身,但每次回源都要重新从存储层读取,TTFB 从几十毫秒变成几秒,加上国内网络环境,体验直接崩了。
根因分析
为什么缓存命中率这么低?
Cloudflare 默认对 HTML 页面不做缓存(或者说 Edge TTL 非常短),理由是动态网站需要实时内容。但对于纯静态站点来说,这个默认策略完全是反的。
具体原因有三:
1. Cloudflare 默认不缓存 HTML
Cloudflare 的设计哲学是"源站最懂自己的内容该缓存多久"。如果源站返回的 Cache-Control 头没有明确指示,CF 对 HTML 的 Edge TTL 只有 2 小时,而且很多情况下直接 bypass。
2. _headers 文件只能控制浏览器缓存
我在项目里配了 _headers(Cloudflare Pages 的响应头配置):
# out/_headers
/*
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
这个配置告诉浏览器"这个页面可以缓存 1 天"。但 Cloudflare 边缘节点自己认不认这个头?不一定。_headers 的输出是响应给浏览器的,对 CF 边缘缓存行为的直接影响有限。
3. Cookie 导致缓存 bypass
Cloudflare 默认行为:如果请求带了 Cookie,就 bypass 缓存。而浏览器访问自己的域名时,如果之前有过交互(比如点击、表单),就会带 Cookie。对于工具站来说,用户用了一个工具后访问另一个工具,Cookie 就带着了。
解决方案
方案选型
| 方案 | 效果 | 成本 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| Cache Rules | ★★★★ | 免费 | 低 | 所有站点 |
| IP 优选 + Worker 代理 | ★★★★★ | 免费 | 高 | 重度 CF 用户 |
| 国内 CDN 回源 | ★★★★★ | ¥50+/月 | 中 | 有备案域名 |
只做 _headers | ★★ | 免费 | 极低 | 辅助手段 |
结论:Cache Rules 是性价比最高的方案,先做这个。
第一步:创建 Cache Rules
去 Cloudflare Dashboard → Caching → Cache Rules → Create Rule。
免费套餐最多 3 条规则,我们正好 3 条。
规则 1:静态资源长缓存(优先级最高)
匹配条件:
HTTP Host equals "utlkit.com"
AND
HTTP Request URI matches one of:
/_next/static/*
/fonts/*
动作:
- Edge TTL → Ignore cache-control header, 30 天
- Browser TTL → Override, 30 天
- Ignore query string ✅
Next.js 的
/_next/static/目录下所有文件都带了内容 hash,文件名变了就是新文件,所以可以大胆设长缓存。字体同理。
规则 2:sitemap 短缓存
匹配条件:
HTTP Host equals "utlkit.com"
AND
HTTP Request URI matches one of:
/sitemap.xml
/robots.txt
动作:
- Edge TTL → Ignore cache-control, 1 天
- Browser TTL → Override, 1 天
规则 3:HTML 页面兜底缓存(最关键)
匹配条件:
HTTP Host equals "utlkit.com"
动作:
- Edge TTL → Ignore cache-control header, 14 天
- Browser TTL → Override, 7 天
- Ignore query string ✅(关键!
/?cat=finance和/共享缓存)
这里有个取舍:Edge TTL 设多长?太短了缓存没意义,太长了部署后用户看不到更新。 Cloudflare Pages 在每次部署后会自动 purge 缓存,所以 Edge TTL 可以设比较长。 浏览器 TTL 设短一些(7 天),防止用户清缓存前一直看旧版。
规则顺序: 静态资源 → sitemap → HTML(兜底放最后)
第二步:Purge Everything
配置完规则后,Caching → Configuration → Purge Everything,清除旧缓存。
第三步:验证
# 第一次请求
$ curl -sI https://utlkit.com/ | grep cf-cache-status
cf-cache-status: MISS ← 正常,首次 MISS
# 第二次请求
$ curl -sI https://utlkit.com/ | grep cf-cache-status
cf-cache-status: HIT ← 成功!
验证几个关键资源:
# 工具页
curl -sI https://utlkit.com/tools/bmi-calculator/ | grep cf-cache-status
# MISS → HIT ✅
# JS chunk
curl -sI https://utlkit.com/_next/static/chunks/main-app-xxx.js | grep cf-cache-status
# MISS → HIT ✅
# CSS
curl -sI https://utlkit.com/_next/static/css/xxx.css | grep cf-cache-status
# MISS → HIT ✅
额外优化:Sentry CDN 本地化
排查过程中还发现一个问题——layout.tsx 里引用了 Sentry 的 CDN:
<script src="https://browser.sentry-cdn.com/8.30.0/bundle.min.js" />
browser.sentry-cdn.com 在国内访问经常超时(有时完全连不上),这个 70KB 的脚本在 <head> 里,会阻塞整个页面渲染。
解决很简单:下载下来放到 public/ 目录:
curl -o public/sentry-bundle.min.js \
https://browser.sentry-cdn.com/8.30.0/bundle.min.js
改引用:
- <script src="https://browser.sentry-cdn.com/8.30.0/bundle.min.js" />
+ <script src="/sentry-bundle.min.js" />
这样这个脚本走 Cloudflare 自己的 CDN,国内速度直接起飞。
⚠️ 注意:Sentry CDN bundle 更新时记得手动替换文件。可以设个提醒每季度检查一次。
效果对比
| 指标 | 优化前 | 优化后 |
|---|---|---|
| Cloudflare 缓存命中率 | 7.62% | 90%+ |
| 首页 TTFB(命中缓存) | 2-7s | 200-400ms |
| LCP P75 | 20s | 2-3s |
| 国内首屏加载 | 经常 10s+ | 稳定 3-5s |
踩坑记录
坑 1:CF 新版界面和旧文档对不上
网上的教程大多是基于旧版 Cache Rules 界面写的,参数名称完全不同。新版界面(2024+)把很多设置拆到了子面板里,找不到 "Cache Level" 和 "Edge TTL" 的具体输入框。
解决方案: Edge TTL 现在是个下拉选项,选 "Ignore cache-control header and use this TTL" 后才能输入具体时间。最大可选 30 天。
坑 2:Ignore query string 不选,缓存被 query string 分裂
工具站的首页经常被访问为 /?cat=finance、/?cat=developer 等。如果不忽略 query string,CF 会把每个不同的 query 当作不同的缓存 key。104 个工具 × 8 个分类 = 800+ 个缓存版本,命中率又掉了。
解决方案: 勾选 "Ignore query string",所有 query 共享同一份缓存。
坑 3:Browser TTL 设太长,部署后用户看不到更新
一开始 Browser TTL 也设了 14 天,结果部署了新版本,同事反馈"怎么还是旧的"。浏览器缓存不受 CF Purge 影响,只能等 TTL 过期。
解决方案: Browser TTL 降到 7 天,Edge TTL 保持 14 天。CF 部署自动 purge Edge 缓存,新访客立刻看到新版,老用户 7 天内逐步更新。
进阶方案
如果你的站点国内访问还是很慢(即使缓存命中了),可以考虑 IP 优选 + Worker 代理 方案:
- 在 DNS 中创建
cdn.yourdomain.com(灰云),CNAME 到第三方优选 IP 池 - 所有子域名灰云 CNAME 到
cdn.yourdomain.com - Pages 本身不支持路由,用一个 Worker 做反向代理桥接
- Worker 走路由模式(支持优选),转发请求到 Pages
这个方案能把国内延迟从 300-500ms 压到 50-100ms,但维护成本也高得多。对于工具站来说,Cache Rules 优化后基本够用了。
总结
Cloudflare Pages 免费套餐 + 3 条 Cache Rules = 足够好的全球 CDN。
关键就三件事:
- 显式告诉 CF 缓存 HTML(默认不缓存)
- 忽略 query string(防止缓存分裂)
- 区分 Edge TTL 和 Browser TTL(Edge 设长,Browser 设短)
免费、简单、效果显著。
📚 本文是 UtlKit 技术系列第 9 篇 — 系列索引 →
- 上一篇:PageSpeed CI →
- 下一篇:[暗色模式 →]
*本文基于 utlkit.com 实际优化经验。站点:utlkit.com