写着每次都要回源,CDN手上那份副本已经躺了9小时

写着每次都要回源,CDN手上那份副本已经躺了9小时
张文保 更新 31 分钟阅读 2,825 阅读
本文目录
  1. 一行Cache-Control里的每个词,究竟是说给谁听的?
  2. 为什么private不是“给浏览器的”
  3. 115个站的首页,这行头到底写了什么?
  4. 为什么一半的站在这行头里一个字都不写?
  5. 47个站对CDN单独说了一句话,浏览器听不见
  6. 这是谁的手笔
  7. 写着每次都要回源,为什么那份副本已经躺了9小时?
  8. 这个区别在什么时候会咬人
  9. s-maxage只有8个站写,其中2个写了等于没写
  10. 把private写在一张公开的CSS上,谁会照做?
  11. no-cache后面还能跟一个字段名,那句话只有CDN会读
  12. 我怎么知道它真的被缓存了,Age这把尺子的边界在哪?
  13. 这把尺子测不到的那一半
  14. 静态资源上那两个出厂值,为什么一个365天一个365.25天?
  15. 这行头写歪了,SEO上到底损失什么?
  16. 自己站上这两拨听众,怎么一次盘清楚?
  17. 常见问题解答
  18. 写了max-age=0, must-revalidate,我的页面到底会不会被CDN存下来?
  19. s-maxage和max-age我都写了,浏览器听哪个?
  20. 不写Cache-Control,是不是就等于不缓存?
  21. private写在CSS、图片这类静态资源上,有什么后果?
  22. 响应头里没有Age,是不是说明这个页面没被缓存过?
  23. CDN-Cache-Control这个头需要我自己加吗?
  24. 怎么快速判断一个响应是从边缘来的还是从源站来的?
  25. 权威参考资料
摘要:115个能正常打开的海外品牌站里,首页响应头有58个一个字的缓存声明都没写,剩下57个写了的,有22个通篇没有一句是说给CDN听的。更反直觉的是另一组:11个站在头里写着max-age=0、必须回源验证,可同一个响应带回来的Age字段显示,CDN手上那份副本已经躺了最长9小时。而47个站在Cache-Control里对浏览器只字未提,却另外发了一个CDN-Cache-Control专门对边缘说“别存”——这个头100%出自同一个建站平台,其余7类平台一个都没有。

缓存这件事,很多人的心智模型是一句话:我在响应头里写多久,东西就存多久。

这句话漏掉了一个前提——你在对谁说话。一个页面从服务器到用户眼前,中间至少站着两拨完全不同的听众:用户自己那台电脑上的浏览器缓存,以及横在中间的共享缓存(CDN节点、反向代理、公司出口的代理服务器)。Cache-Control那一串用逗号隔开的词,不是一句话说给一个人听,而是一堆词各自认领各自的听众。

有的词两拨都读,比如max-age。有的词只有共享缓存会读,浏览器看见了也当没看见,比如s-maxage、proxy-revalidate。有的词反过来,主要靠浏览器兑现,中间那层大多不认,比如immutable。你写下一整行,实际上是同时朝两个方向喊话,而每一拨只挑属于自己的那几个词听——没点到名的那部分,他按自己的规矩办,不报错、不提示、不在任何监控面板上留痕。

这就是这篇要量的东西:一条声明的作用域到底覆盖到哪儿,边界之外那一块发生了什么。我把131个海外品牌站的首页、以及每个站上的一个CSS、一个JS、一张图片、一个网站图标和robots.txt,逐个抓下来读了一遍响应头,顺便用Age字段反查“谁真的把它存下来了”。

先说一句结论性的:怎么配缓存头这类文章已经很多,缺的不是配置模板,而是把“这句话是对谁说的”这一层拎出来单独对一遍。同一份模板抄到不同架构上,听众换了,效果就完全不是一回事。

一行Cache-Control里的每个词,究竟是说给谁听的?

先把受众这件事摊开。RFC 9111把缓存分成两类:私有缓存(private cache,实际上就是浏览器为单个用户存的那份)和共享缓存(shared cache,为多个用户存同一份,CDN和反向代理都属于这一类)。规范给每条指令都注明了适用对象,只是这份对照表很少有人完整读过。

指令浏览器(私有缓存)CDN/代理(共享缓存)115个首页里出现
max-age34
no-store29
no-cache31
must-revalidate34
public基本无感读,是它的主要听众21
private不针对它读,含义是“你别存”11
s-maxage明确忽略读,且优先于max-age8
proxy-revalidate明确忽略0
immutable多数不兑现0(子资源上37处)
stale-while-revalidate部分实现读,主战场9(全样本34个站)

注意最后一列的形状:出现频次最高的四个词,全是两边都读的通用词;而专门写给共享缓存的那几个,加起来的出现次数还不如max-age一个多。这不是巧合,后面几节会看到它是一种系统性的偏斜。

为什么private不是“给浏览器的”

这是最常被讲反的一个词。private读起来像是在对浏览器说“你可以私下存着”,但它真正的收信人是中间那一层:它是在告诉共享缓存“这份响应带着某个特定用户的信息,你不许存”。浏览器本来就只服务一个用户,它不需要这句话的许可。

所以当你在一个所有人都拿到同一份字节的公开CSS上写private,你并没有给浏览器多一点什么,你只是把CDN从这条链路上赶走了。这个错位在样本里出现了不止一次,后面有一节专门算这笔账。

115个站的首页,这行头到底写了什么?

先把分母说清楚。131个站里,16个对这个客户端根本不给正常响应:11个直接403,4个连续限流返回429,1个返回418。这16个不进任何统计——把“我没拿到”混进“它没有”,是这类审计最常见的一种自伤,也是我上一轮实测吃过的亏。

剩下115个站是全文所有比例的分母。

首页Cache-Control站数占115
一个字都不发5850.4%
发了,但没有一句是给共享缓存的2219.1%
发了,其中1条给共享缓存2320.0%
发了,其中2条给共享缓存76.1%
发了,其中3条给共享缓存54.3%

把前两行加起来是80个站,接近七成。这七成的共同点是:在首页这个最值钱的响应上,他们从没对中间那一层说过一个字。

还有一个顺手捡到的小样本。那11个返回403的站里,有6个连拒绝页都带着完整的缓存头,写的是private, max-age=0, no-store, no-cache, must-revalidate, post-check=0, pre-check=0。这一串里的最后两个词是IE 5时代的私有扩展,现代浏览器和CDN都不认识它——一句说给一个已经不存在的听众的话,跟着模板原样传了二十年。

为什么一半的站在这行头里一个字都不写?

不写会怎样?规范里有答案,只是这个答案不太让人安心。RFC 9111允许缓存在没有明确寿命声明时自己估一个“启发式寿命”,通常的做法是拿响应时间减去Last-Modified,取其中的一个比例(常见实现取十分之一)当作可以放心复用的时长。

问题是这个估算需要输入。我数了那58个不发Cache-Control的站:

启发式算寿命需要的输入58个站里有几个
带Last-Modified(唯一能直接喂给启发式的字段)3
带Expires0
带ETag(只能做验证,算不出寿命)48
三样能定寿命的输入一个都没有8

也就是说,绝大多数沉默的站,连让中间层“猜”的材料都没提供。剩下的空白由每一家CDN自己的默认策略填上——Cloudflare对HTML默认不缓存,Fastly和Varnish的出厂配置又各有各的算法,同一个站放在不同的边缘上,行为可以完全不同。这条缝隙的宽度不由你决定,由你签的那份服务合同决定。

值得单独说一句的是那48个带ETag却不带任何寿命声明的站。ETag解决的是“我手上这份还新鲜吗”,它需要缓存先存下一份、再拿着它去问。可你都没说能不能存,这个ETag就只剩下给浏览器省一次完整下载的价值了,中间那一层压根没被邀请进这个流程。想把条件请求这条路走通,得先把存不存这件事说清楚,这一层的收益账在304状态码那次142个站的实测里算过。

47个站对CDN单独说了一句话,浏览器听不见

这是这轮实测里最干净的一组数字。

除了Cache-Control,还有一个专门写给边缘层的头叫CDN-Cache-Control。它的语义很直接:如果一个响应同时带着两个头,CDN读后者、浏览器读前者,各拿各的那一份。115个站里,有48个发了这个头。

取值分布是:48个全部写着no-cache, no-store,一字不差。

而其中47个,在Cache-Control那一栏里什么都没写。

把这两句话拼在一起读:这47个站对浏览器保持沉默,同时扭过头去对CDN说了一句非常明确的“别存”。同一件事,两个听众,两种完全不同的待遇。

这是谁的手笔

“某某跟着某某走”这种归因,只量支持自己的那一组是不作数的,得同时量不具备这个特征的那一组。我把上一轮实测留下的建站平台标签接过来,对全部115个站做了分组:

建站平台样本站数发CDN-Cache-Control发出率
Shopify664872.7%
Salesforce1000%
Next.js自建900%
Magento300%
BigCommerce200%
WordPress100%
指纹认不出来的2400%

对照组干净得有点吓人:除了一个平台,其余6类加上认不出的那批,49个站,一个都没有。这句“别存”不是这47个品牌各自的技术决策,它是出厂时就写在响应里的,绝大多数店主这辈子没见过它。

这个头本身是个好东西——把边缘策略和浏览器策略彻底分开,正是在边缘那一层改SEO这套打法的地基。只是当它以出厂默认的形式写着“别存”、而店主又完全不知情的时候,这个地基就成了一堵墙。想动它得先知道自己那家CDN认哪个头,Cloudflare那套缓存规则的迁移路径是个可比的样本。

唯一一个两边都发、且说的不一样的是fahertybrand:Cache-Control写private, no-store,CDN-Cache-Control写no-cache, no-store。两句话意思相近,但显然出自不同的笔。

写着每次都要回源,为什么那份副本已经躺了9小时?

现在说这轮实测里最反直觉的一组。

有11个站的首页写着max-age=0或者no-cache,同时带回了Age字段。绝大多数人读这一行的理解是“别缓存,每次都来问我”。但Age说的是另一回事——它是共享缓存自己填的,含义是“这份副本在我这儿已经放了多少秒”。它出现,就等于中间那一层承认:我存了。

更整齐的是,这11个里有10个写的是一模一样的public, max-age=0, must-revalidate

站点Age(秒)换算它的Cache-Control
traeger.com326429小时4分public, max-age=0, must-revalidate
babybjorn.com315668小时46分public,max-age=0,must-revalidate
ruggable.com249641分public, max-age=0, must-revalidate
bigcommerce.com2233分43秒public, max-age=0, must-revalidate
anker.com1973分17秒public,max-age=0,must-revalidate
vuoriclothing.com1903分10秒public,max-age=0,must-revalidate
lookfantastic.com1402分20秒max-age=0, s-maxage=900, stale-while-revalidate=300
soundcore.com1202分public,max-age=0,must-revalidate
suitsupply.com751分15秒public, max-age=0, must-revalidate
eufy.com1414秒public,max-age=0,must-revalidate
kotn.com1212秒public, max-age=0, must-revalidate

这不是CDN在违规。规范里,max-age管的是“这份东西多久之后算过期”,must-revalidate管的是“过期之后不许直接端上来,必须先去问一次源站”。两句话加起来的准确意思是:可以存,但从存下的那一秒起就算过期,每次要用之前都得回源验证一遍。它从头到尾没说过“不许存”。

真正说“不许存”的那个词叫no-store。而这11个站里,一个都没写。lookfantastic那一行更值得看一眼:它把s-maxage写成900,等于亲口请边缘存15分钟,而Age显示副本已经放了140秒——完全在它自己许可的范围内。

这个区别在什么时候会咬人

存与不存的差别,平时确实看不出来——每次都回源验证,用户拿到的内容总是新的。但两个场景下它会突然变得很重要。

第一个是回源不通的时候。副本还在边缘手上,源站挂了或者超时,缓存可以按照配置把这份过期副本直接端出去(stale-if-error就是干这个的)。这时候用户看到的是一个也许几小时前的页面,而你的监控显示源站已经502了。

第二个是这份副本里带了个人化内容的时候。max-age=0没有阻止它被存进一个所有人共用的池子,如果某个响应里恰好夹了上一个访客的购物车或者地区信息,它就有机会被端给下一个人。想避免这件事,得用private把共享缓存请出去,或者干脆用no-store。写max-age=0, must-revalidate解决不了它。

还有两个站更彻底:innisfree和assos的首页一个缓存声明都没有,却分别带回了29119秒和1017秒的Age。没人告诉中间层能不能存,中间层自己做了决定。

反过来的坑也存在,而且更隐蔽:有些运行时会背着你往响应里塞缓存头。PHP只要执行到一句开启会话的代码就自动发三个头,其中一个写着1981年的日期,效果是把整页从所有共享缓存里踢出去。你在配置文件里怎么写都不管用,因为这句话不是你说的。

s-maxage只有8个站写,其中2个写了等于没写

stale-while-revalidate与stale-if-error这两个扩展是另一份文件定义的,而s-maxage是这套语法里唯一一个“专款专用”的词:它给共享缓存单独指定一个寿命,且优先级高于max-age,浏览器看见它会直接跳过。它存在的全部意义,就是让你对两拨听众说两个不同的数字——边缘可以存久一点,浏览器那边短一点,改内容的时候只需要清边缘。

115个站里写了它的有8个,一个不多。

站点max-ages-maxage两者的关系
ikea.com9002592000边缘存30天,浏览器15分钟,差2880倍
lookfantastic.com0900浏览器不留,边缘留15分钟
on.com0180浏览器不留,边缘留3分钟
arcteryx.com120600边缘5倍于浏览器
braun.com18013601边缘2倍于浏览器
burrow.com9001800边缘2倍于浏览器
bollandbranch.com18001800两个数一样,等于没写
quince.com00两个数一样,等于没写

前6个是把这个词用对了的样子,尤其是ikea那一组:让边缘扛整整一个月,浏览器只留一刻钟,内容更新时只要清一次边缘,全球用户下一次访问就是新的。这是共享缓存最值钱的用法,115个站里认真做了这件事的是个位数。

后2个属于另一种情况:既然两个数字一样,那么删掉s-maxage,行为一个字节都不会变。它出现在那里,多半是从某份模板里抄来的。

把边缘和浏览器的时长拆开定,是边缘缓存TTL分层里最先该做的一步;如果源站前面还压着一层自己的全页缓存,那就是三段时长要对齐,Nginx全页缓存那套配置里可以看到这三段是怎么互相牵制的。

把private写在一张公开的CSS上,谁会照做?

前面说过private的收信人是共享缓存。那么它出现在什么地方最不该出现?答案是所有人拿到的都是同一份字节的静态资源。

样本里,CSS文件上写了private的有10个站,图片2个,JS 2个,robots.txt 4个。其中有9处更有意思,因为它们把两个互相拆台的词写进了同一行:

站点CSS上的Cache-Control建站平台
buckmason.comprivate, max-age=600, stale-while-revalidate=604800Shopify
dollarshaveclub.comprivate, max-age=600, stale-while-revalidate=604800Shopify
vuoriclothing.comprivate, max-age=600, stale-while-revalidate=604800Shopify
wusthof.comprivate, max-age=600, stale-while-revalidate=604800Shopify
charleskeith.comprivate, max-age=600, stale-while-revalidate=604800Salesforce
sostrenegrene.comprivate, max-age=600, stale-while-revalidate=604800认不出
casetify.comprivate, max-age=86400, stale-while-revalidate=604800认不出
insta360.comprivate, max-age=86400, stale-while-revalidate=604800Next.js
lookfantastic.comprivate, max-age=86400, stale-while-revalidate=604800Salesforce

stale-while-revalidate的意思是“过期之后先把旧的端出去,同时在后台悄悄去拿新的”,这个动作只有共享缓存做得漂亮——它替成千上万人做一次后台刷新。而private刚刚把共享缓存赶出了门。一行字里,前半句解雇了后半句要用的人。

更值得注意的是最后一列:这9个站跨了4类建站平台,说明这句话不是某个平台的出厂值,而是某一层CDN或者某一份加固模板留下的。同一句错话能横跨4个技术栈,靠的不是巧合,是复制粘贴。

代价是实打实的字节。静态资源不进共享缓存,每一次访问都得从源站原路取回,而这批文件恰恰是全站体积最大的那部分。同样一批字节反复回源要付多少,压缩协商那次实测算过一笔类似的账:出厂配置只压了HTML一种类型,其余的全额计费。

no-cache后面还能跟一个字段名,那句话只有CDN会读

整个样本里最冷门的一处写法,来自quince.com。它的CSS、JS和网站图标上都写着:

public, max-age=31536000, no-cache="Set-Cookie"

这个带引号的参数形态在规范里是有定义的:它的意思不是“整份别缓存”,而是“这份可以缓存,但复用给别人之前,请把Set-Cookie这个头字段摘掉”。用途很实在——静态资源上偶尔会挂一个会话Cookie,如果整份存下来发给下一个人,那个Cookie就跟着串了出去。

但规范同时写明:这种带参数的形式只对共享缓存有约束力,私有缓存忽略参数,把它当成一个裸的no-cache来处理

于是同一行字在两拨听众耳朵里是两个意思。CDN听到的是“存一年,摘掉那个头”;浏览器听到的是“每次用之前都得验证一次”,那个一年的max-age被前面的no-cache直接压掉了。写这一行的人多半只想着前一半。

这类“同一份声明在不同读者那里解析出不同结果”的形态,在响应头这个层面反复出现,248个死字段那次盘点里还能看到更多化石。

我怎么知道它真的被缓存了,Age这把尺子的边界在哪?

上面好几节的结论都压在Age这一个字段上,所以得先证明这把尺子是准的。Age这个头的定义本身很短:由共享缓存填写,单位是秒,含义是这份副本自被存下起过了多久。

方法很笨:同一个URL,隔6秒再打一次,看Age涨不涨、涨多少。如果它是一个诚实的计时器,两次的差值应该约等于我等待的时间。

两次都带Age的24个站结果
Age变大20个
Age不变3个
Age变小1个
变大那20个的涨幅最小6秒,中位6秒,最大7秒

中位数正好等于我等的秒数,这把尺子可以用。3个不变的是Age本来就极小的站(0秒或1秒),第二次仍在同一秒内。

唯一变小的那个是babybjorn:第一次31566秒,第二次29244秒,倒退了将近39分钟。副本不会返老还童,唯一的解释是两次请求落在了不同的边缘节点上,各自存了各自的一份,年纪不一样。这本身也是个结论:你测到的Age是某一个节点的年纪,不是全球的。

这把尺子测不到的那一半

更要紧的是反过来的情况:不带Age,不等于没被缓存。

115个站里,首页带Age的只有24个,剩下91个不带。但这91个里有72个带着CDN自己的命中标记,取值分布是DYNAMIC 61个、cloudfront的各种MISS 7个,剩下几个是TCP_HIT、HIT一类。这61个DYNAMIC说的是“这次没走缓存,直接回源了”——CDN在场,只是这一次没存。

所以前面那些数字要这么读:11个站是确凿被存过的,而不是“只有11个站的响应会被存”。剩下的多数站,我这一次的请求恰好没命中,或者那家CDN干脆不加Age头。测量能证明发生过什么,证明不了没发生过什么。

要把“被存过多少次”这件事量准,单次抓取不够,得看一段时间的访问记录。服务器日志是唯一说真话的地方,只是那里面的水也不浅——日志里每5次自称谷歌的抓取就有1次IP对不上,先把样本清干净再谈统计。

静态资源上那两个出厂值,为什么一个365天一个365.25天?

子资源那一层的数字比首页整齐得多,整齐到能一眼看出模板的边界。

max-age取值换算出现在几个站上平台分布
31557600365.25天58Shopify,58个全部是
31536000365天43横跨7类平台

31536000是365乘86400,谁都写得出来。31557600多出来的那21600秒是6小时——也就是把一年按儒略年365.25天算,闰年那四分之一天摊进去了。这个值在58个站上出现,而且平台归因是满分:全部来自同一家。

这类满分归因在响应头审计里出现过不止一次。上一轮那个91.3天的HSTS默认值也是同样的形状:没人管的时候,几十个站异口同声;一旦有人动手改,就改出好几种互不相同的说法。

顺带一提,immutable这个词在样本里出现37处,全在子资源上(CSS 17、JS 14、图片4、图标2),首页一个都没有——这一点大家倒是没搞错,immutable的意思是“这个URL的内容永远不会变”,对一个天天更新的首页说这话等于自找麻烦。

“出厂值原封不动,除非有人动过”这个形态,在响应头之外的地方也一样成立。robots.txt分组不继承那次157个站的实测量的是另一种边界——写在一个分组里的规则管不到另一个分组,而绝大多数站的第二个分组是空的。声明的边界在哪儿,通常不是站长划的,是标准划的。

这行头写歪了,SEO上到底损失什么?

把上面几节的账合并起来看,落到搜索这一侧有三笔。

第一笔是抓取端的响应速度。抓取工具的取样和真人不一样,它对同一批URL的重复访问频率相当高,而边缘命中与回源之间的首字节差距通常在几百毫秒的量级。首页这类被反复抓取的页面如果始终不进共享缓存,这个差价每天都在付。首字节这条线怎么拆成可归因的几段,TTFB与多层缓存那篇拆过一遍。

第二笔是自相矛盾带来的不确定性。当私有指令、共享指令和CDN专用头三份声明同时存在又互相打架时,最终行为取决于路上碰到的是哪一家实现。同一个URL在不同时间点被抓到不同新鲜度的版本,这种波动很难在日志里定位,Vary那次实测量到的是同一个病灶的另一个切面:声明的变化维度和实际的变化维度对不上。

第三笔容易被忽略:缓存策略会改变你看到的一切诊断结果。用工具测一个页面,测到的是边缘那份副本还是源站直出,速度报告可以差出一个数量级。这也是为什么同一个站在不同测速工具上得分对不齐——它们打到的不是同一份东西。测量工具本身的偏差怎么排除,HEAD与GET那次对照里有一份现成的清单。

这三笔账都落在同一个终点上,就是页面打开有多快。速度和排名之间的关系被夸大过很多次,真实的权重和优先级页面速度那篇算过;而响应头这一层能撬动的具体开关,X-Robots、缓存与Vary那份机制拆解列得比较全。

自己站上这两拨听众,怎么一次盘清楚?

不需要什么复杂工具,一条命令加4个问题就够了。

先把首页和一个静态资源的响应头都取回来(记得用GET而不是HEAD,有些服务器对HEAD会少给几个字段),然后按顺序问:

  1. Cache-Control这一行里,有没有任何一个词是专门写给共享缓存的?找s-maxage、public、private、proxy-revalidate,MDN那份指令对照表逐条标了谁读谁不读。一个都没有,说明你从没对中间那层表过态。
  2. 有没有Age字段?有,说明确实被某一层共享缓存存过;数值多大,就是那份副本当时的年纪。没有,再看有没有CDN自己的命中标记头,别把“没测到”当成“没发生”。
  3. 如果你的意图是“不要存”,写的是no-store还是max-age=0?后者不阻止存,只是让它立刻过期。这两个词的差别在个人化内容上是安全边界,不是风格问题,MDN那份缓存指南把它们的分工画成过一张图。
  4. 静态资源上有没有private?有就删掉,你正在把CDN挡在门外,然后自己付回源的流量费。

如果站跑在Apache上,这4件事连同重写规则、规范网址和HSTS往往是同一个文件里的邻居,.htaccess那份分层治理清单把它们的先后顺序排过一遍;跑在Nginx上则要多留一分神,reload每次都通过、抓取却每8次撞一次跳转这种事,配置检查工具是不报的。

还有一件顺手能做的:既然缓存的目的是少传一次字节,那就顺着这条线把条件请求也接上。没改过的页面被对爬虫重发几百遍,症结和这里是同一个——你没告诉对方能不能存,它就只能每次全量拿。

4个问题跑完,如果发现要动的地方不止一处,别急着一次全改。缓存这一层的改动有个特点:生效延迟长、回滚成本高、验证窗口窄。保哥自己那台服务器上有过一次很小的教训——为了确认发文后缓存真的清干净了,去数了一遍Redis库里到底有几个键,才发现清的那一层和挡在最前面的那一层根本不是同一个,页面照旧是旧的。那次的排查顺序整理在多层缓存清理顺序那篇里,先分清楚有几层、每层归谁管,再决定改哪一行。

把顺序倒过来说更好记:先确定你有几拨听众,再决定对每一拨说什么,最后才是写那一行字。多数站的问题不在于写错了词,而在于从头到尾只想着一拨人。

常见问题解答

写了max-age=0, must-revalidate,我的页面到底会不会被CDN存下来?

会。这一行的准确含义是“可以存,但存下来就算过期,每次用之前必须回源验证”。实测里有11个站写着它,同时带回了Age字段,最长的那份副本已经在边缘放了9小时。如果你的意图是完全不进共享缓存,要写的词是no-store;如果只是不想让共享缓存服务别人,写private。

s-maxage和max-age我都写了,浏览器听哪个?

浏览器只听max-age,它会直接忽略s-maxage;CDN和代理反过来,s-maxage对它们优先级更高。这就是这个词存在的意义——让两拨听众拿到两个不同的数字。如果你把这两个值写成一样的,那么删掉s-maxage行为完全不变,实测样本里有2个站就是这种写法。

不写Cache-Control,是不是就等于不缓存?

不是,正好相反:不写等于把决定权交给对方。规范允许缓存在没有明确声明时自己估一个寿命,通常拿Last-Modified来推算。实测中一半的站(58个)在首页上什么都不写,而其中只有3个提供了Last-Modified——剩下的连让对方估算的材料都没有,具体行为完全取决于那家CDN的默认策略。

private写在CSS、图片这类静态资源上,有什么后果?

后果是共享缓存不再为你分发这份文件,所有请求都要回到源站。你付了CDN的钱,却只用到了转发功能。实测里有10个站的CSS上写着private,其中9个还同时写了stale-while-revalidate——而后者恰恰是只有共享缓存才会执行的动作,一行字里前后互相拆台。

响应头里没有Age,是不是说明这个页面没被缓存过?

不能这么推。Age只由共享缓存自己填,没命中的时候本来就不会有;而且有些CDN即使命中也不加这个头。实测的115个站里,91个不带Age,但其中72个带着CDN自己的命中状态标记,说明CDN确实在链路上,只是这一次没走缓存。判断“有没有被缓存过”要靠多次取样,单次结果只能证明发生过什么,证明不了没发生过什么。

CDN-Cache-Control这个头需要我自己加吗?

看你用哪家CDN,以及你是不是真的需要对两层说不同的话。它的价值在于把边缘策略和浏览器策略彻底分开——比如让边缘存一小时、浏览器完全不存。实测中发这个头的48个站全部出自同一个建站平台,取值一字不差,说明它更多是平台的出厂设定而非店主的选择。自建站点如果没有明确的分层需求,先把Cache-Control里的s-maxage用起来更划算,兼容面也更广。

怎么快速判断一个响应是从边缘来的还是从源站来的?

三个信号一起看:Age字段(有值说明是缓存里的副本)、各家CDN自己的状态头(cf-cache-status、x-cache、x-vercel-cache这类,取值是HIT还是MISS或DYNAMIC)、以及连续几次请求的首字节时间是否稳定。单看任何一个都可能被误导,尤其是DYNAMIC这种取值,它说的是“这次没缓存”,不是“这个站没接CDN”。

权威参考资料

分享到
标签
版权声明

本文标题:《写着每次都要回源,CDN手上那份副本已经躺了9小时》

本文链接:https://zhangwenbao.com/cache-control-directive-audience-scope-audit.html

版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0

继续阅读
发表评论
分享到微信 或在下方手动填写
支持 Ctrl + Enter 提交