你的页面明明被缓存了,为什么用户还是每次都要等?——No-Vary-Search 把这件事说清楚了
你配了 CDN,配了 Service Worker,配了 HTTP Cache-Control,上线前 Lighthouse 分数也漂亮。但用户从 Google 广告点进来等了半天,从邮件链接点进来又等了半天,从小红书卡片点进来——又等了半天。
同一个页面,同一个响应体,只是 URL 上多挂了一个 ?utm_source=google 或者 ?ref=chatbot。HTTP 缓存不认识这件事:URL 差一个字符,就是两个缓存 key,就要发两次请求。
No-Vary-Search 就是来解决这个问题的。
这个问题是怎么来的
HTTP 缓存默认把完整 URL 当作缓存的 key。只要 URL 字符串不一样,缓存就认为这是两个不同的资源,不管它们返回的 body 是不是一模一样。
/products
/products?utm_source=google
/products?utm_source=chatgpt
这三个在服务端返回的 HTML 完全相同,但缓存里是三份独立的条目。第三次访问 —— 不管你之前访问过多少次 —— 都是一次新的请求。
这对缓存来说是一种保守策略:宁可多存几个,也不要把不同的内容错误地当作相同的。但问题是,UTM 参数、ref 参数、session ID 这些从来不改变页面内容,却把缓存切得七零八落。
现实中的代价有多大?
根据 web.dev 的数据,在一个中等规模的电商网站里,带有 UTM 参数的 URL 占了总流量的 40%~60%。如果这些请求全都不走缓存,意味着 CDN 和源站每处理 100 个页面请求,就要重复处理 60 个其实已经缓存过的。
No-Vary-Search:让服务器声明哪些参数不影响内容
No-Vary-Search 是一个 HTTP 响应头,服务器用它告诉浏览器:「这个响应里,某些 query 参数是不影响返回内容的,它们不应该分裂缓存 key。」
最基础的用法:
No-Vary-Search: params=("utm_source" "utm_medium" "utm_campaign" "utm_content" "utm_term")
这句话的意思是:utm_source、utm_medium 这几个参数不影响响应内容,浏览器遇到带这些参数的 URL 时,直接用不带参数的缓存条目。
Chrome 141(2026 年初)把这个能力扩展到了完整的 HTTP 磁盘缓存层面——不仅仅是 Speculation Rules 预取了,普通导航也适用。
还有一个参数顺序的问题:
/products?size=M&color=red
/products?color=red&size=M
这两个 URL 内容的响应完全一样,但如果缓存的 key 包含了顺序,这两个就会命中不同的条目。加上 key-order 可以告诉浏览器:参数顺序也不影响缓存 key。
No-Vary-Search: params=("utm_source" "utm_medium"), key-order
三种声明方式:
# 精确指定哪些参数忽略
No-Vary-Search: params=("utm_source" "ref")
# 所有参数都忽略
No-Vary-Search: params
# 除这些参数外都忽略
No-Vary-Search: except=("page" "sort")
和 Speculation Rules 的配合:这件事才是真正重要的
预取(prefetch)和预渲染(prerender)本来是浏览器帮你提前加载下一页的机制。Speculation Rules API 让前端可以声明「用户悬停的链接应该预取」:
<script type="speculationrules">
{
"prefetch": [{
"source": "document",
"where": { "href_matches": "/*" },
"eagerness": "moderate"
}]
}
</script>
但这里有个致命 bug:用户点击之前,URL 可能被加上了 UTM 参数或 ref。预取缓存的 key 是 https://example.com/offer,但用户实际导航到了 https://example.com/offer?utm_source=newsletter——这两个 URL 不一样,预取白做了,用户还是要等完整的请求。
No-Vary-Search 修好了这个 bug。 有了它,浏览器知道带 UTM 参数的 URL 和不带参数的 URL 是同一个缓存条目,预取命中率会大幅提升。
根据 CSS Wizardry 的测试,配了 No-Vary-Search + Speculation Rules 的营销页面,第二次访问的 TTWR(Time to First Byte)可以降低 80% 以上。
落地怎么做
第一步:在 CDN 或源站加响应头
Nginx(通过 add_header):
location / {
add_header No-Vary-Search 'params=("utm_source" "utm_medium" "utm_campaign" "utm_content" "utm_term" "gclid" "fbclid" "msclkid"), key-order';
}
Vercel(vercel.json):
{
"headers": {
"/**": {
"no-vary-search": "params=(utm_source utm_medium utm_campaign), key-order"
}
}
}
Cloudflare(Page Rules,2026 年已支持):
No-Vary-Search: params=("utm_source" "utm_medium" "utm_campaign"), key-order
第二步:配 Speculation Rules
<script type="speculationrules">
{
"prefetch": [{
"source": "document",
"where": { "href_matches": "/*.html" },
"eagerness": "moderate"
}]
}
</script>
第三步:验证
在 Chrome DevTools → Application → Speculative Loads 面板里,预取命中的请求会标注 (prefetch hit)。另外,用 curl 查原始响应:
curl -sI https://your-site.com/ | grep -i no-vary-search
浏览器支持情况
| 浏览器 | 支持状态 |
|---|---|
| Chrome | 141+(HTTP 磁盘缓存),旧版本仅支持预取/预渲染路径 |
| Edge | 与 Chrome 同步 |
| Safari | 尚无公开实现时间表 |
| Firefox | 尚无公开实现时间表 |
这是一个纯优化——不支持的浏览器会回退到按完整 URL 作为缓存 key,没有功能损失,只是错过缓存优化。
常见的坑
1. 声明了会影响内容的参数
No-Vary-Search: params=("page" "sort") # 危险!
如果 page 参数会改变响应内容,这样做会导致所有分页都返回第一页的缓存——这是缓存污染,不是优化。配之前要先确认哪些参数真的不影响内容。
2. 逗号分隔而不是空格分隔
语法必须是空格分隔的字符串,不是逗号:
# 错误
No-Vary-Search: params=("utm_source", "utm_medium")
# 正确
No-Vary-Search: params=("utm_source" "utm_medium")
3. No-Vary-Search 不是 canonical 的替代品
它只管缓存行为,不管搜索引擎怎么索引 URL。两者都要配。
下一步
如果你的站点还在用传统 CDN 配置,或者还没配 Speculation Rules,做这两件事:
-
加上 No-Vary-Search 响应头:先用精确列表模式(params=(“utm_source” …)),而不是 params(全局忽略),先小范围验证一周,确认没有问题再扩大。
-
配上基础的 Speculation Rules:即使是 moderate eagerness 的 prefetch,也能让第二次访问的 TTFB 降低一个数量级。结合 No-Vary-Search,UTM 来源的访问者也能享受到缓存收益。
这是一个 CDN 和前端一起配合的优化,改动小,收益明确。
评论区
登录后可评论。