Prismic 优化检测 免费在线检测工具
首页 › 笔记 › Cloudflare 显示「已缓存」,可它每次都回源:一个 Vary 头的事

Cloudflare 显示「已缓存」,可它每次都回源:一个 Vary 头的事

我是 Cody Wang,前端工程师,平时做 Prismic / SvelteKit / Next.js 网站的开发和性能优化。

这篇讲我自己踩的一个坑:面板上缓存规则明明是开的、描述写得明明白白,可每个访客的请求还是全打到源站。查了两小时,根因是一个我不记得自己写过的响应头。

#1. 先看现象:面板说开,响应头说没开

我在 Cloudflare 里给自家的检测工具站建了一条 Cache Rule,名字就叫「Cache HTML at the edge (5 min)」,规则是 enabled。逻辑上没问题:只看 GET,带 pa_lang cookie 的请求绕过(那是我站上的语言 cookie),静态资源排除掉。

然后我按老习惯抓了一下响应头:

$ curl -s -D - -o /dev/null https://example.com/
HTTP/2 200
cache-control: public, max-age=300
cf-cache-status: DYNAMIC

DYNAMIC。这意思是「这个响应不参与缓存」,请求每次都回到源站。规则在那儿,但没生效。

顺手对比一下首字节时间,5 次采样:0.47s、0.48s、0.47s、1.23s、2.60s。同一个页面、同一个访客视角,抖动 5 倍。这就是「全量回源」的体感:源站那台 2 核小机器,忙的时候慢一点很正常。

#2. 再找原因:不是规则写错了,是源站不让缓存

排查顺序我建议固定成三步,别猜:

  1. curl -s -D - -o /dev/null <URL>,看 cf-cache-status 和 age
  2. 同一条命令看源站给了什么 Cache-Control
  3. 看有没有 Vary

我卡在第 3 步。源站的每个响应都带着:

Vary: Cookie

Cloudflare 的逻辑很直接:你既然声明「这个响应随 Cookie 变化」,我就没法只按 URL 存一份副本,于是干脆不缓存。改成 Cache Everything 也不行,Vary 在它前面就判完了。

而这个 Vary 是我自己加的,理由当时听起来很合理:站上有中英两版,语言优先取 ?lang=,其次读 pa_lang cookie,所以「同一个 URL 会返回两种语言」,加个 Vary: Cookie 是为了防止把中文页发给英文访客。

问题是它加得太宽了 —— 我给所有响应都加了,包括那些根本没有 cookie、语言完全由 URL 决定的匿名请求。

关键的一句判断:Vary 声明的不是「我读过 cookie」,而是「我的响应体因此不同」。

匿名访客(不带 pa_lang)进来,语言一定是我给的默认值,URL 相同 → 响应体相同 → 这个响应不需要 Vary,可以放心让边缘缓存。只有真的按 cookie 渲染过的响应,才需要 Vary: Cookie。

落到代码上就是记一下语言从哪来:

if want in ("zh", "en"):          # ?lang= 显式指定
    self._lang_from_cookie = False
    return want
m = re.search(r"pa_lang=(zh|en)", cookie_header)
if m:                              # 真的读了 cookie
    self._lang_from_cookie = True
    return m.group(1)
self._lang_from_cookie = False     # 匿名,默认语言
return "zh"

然后发送响应头时按这个标志决定:

if getattr(self, "_lang_from_cookie", False):
    self.send_header("Vary", "Cookie")

改完立刻能看到区别 —— 注意要用 GET,curl -I(HEAD)Cloudflare 一律回 DYNAMIC,我第一次就是被 HEAD 骗的:

$ curl -s -D - -o /dev/null https://example.com/
cache-control: public, max-age=300
age: 23
cf-cache-status: HIT

#4. 前后差多少(同一个站,同一台机器)

| 指标 | 改前 | 改后 | |---|---|---| | cf-cache-status | DYNAMIC(全量回源) | HIT | | PageSpeed 性能(移动端) | 96 | 99 | | 源站响应 server-response | 580 ms | 200 ms | | Speed Index | 4.5 s | 2.0 s | | 首字节(本机采样 5 次) | 0.47–2.60 s | 0.63–1.33 s(本机到边缘的握手占大头) |

最后一行值得说一句:本机测出来的 TTFB 改善有限,因为我在国内、握手到 Cloudflare 边缘就要 0.23–1.7 秒,跟站点本身没关系。判断站点快慢看 server-response,不要看自己 curl 的总时间。

#5. 顺带修的第二个坑

查这个的时候我发现静态资源也在裸奔:/static/app.js(53 KB)返回的是

cache-control: no-store

因为静态文件那个分支只给字体和图片设了长缓存,JS/CSS 落到了默认分支。没有内容哈希的文件名不敢设 immutable,所以我给的是 600 秒 + 一天 stale-while-revalidate:

cache-control: public, max-age=600, stale-while-revalidate=86400

#6. 什么时候你该保留 Vary

别把这篇当成「Vary 是坏的」。该留的两种:

  • 响应体确实随 cookie 变(登录态、购物车、A/B 分流)
  • 或者预算不够改架构,那就老实接受「不缓存」,别指望规则生效

更彻底的做法是把语言放进 URL(/en/、/zh/),这样同一个 URL 只有一种语言,Vary 完全不需要,CDN 和搜索引擎都省心。我这个站当年选了 ?lang=en,现在还在还这笔账 —— 这是另一个话题了。

#一句话

面板上的绿勾不是证据,响应头才是。三个字段就够:cf-cache-status、age、Vary。