静态资源带上哈希之后,一年后一半地址都404了

静态资源带上哈希之后,一年后一半地址都404了
张文保 更新 21 分钟阅读 2,935 阅读
本文目录
  1. 这个实验是怎么做的,为什么必须先量今天?
  2. 第一步:从存档里取原件
  3. 第二步:把当年页面上的地址逐条拿到今天来请求
  4. 第三步:先量今天,再量去年
  5. 一年前那份首页,今天还能跑起来吗?
  6. 但平均数掩盖了两头
  7. 掉的是谁的文件?
  8. 把口径再收紧一点
  9. 按文件类型再切一刀,掉得最狠的是样式表
  10. 同一个域名下,为什么两个目录的命不一样?
  11. Next.js那批更极端
  12. 给缓存加的那串指纹,为什么反而让文件更早消失?
  13. 机制其实很直白
  14. 那些取不到的,究竟是什么形态?
  15. 404不是零星漏掉,是整批一起没
  16. 这件事什么时候真的会伤到你?
  17. 渲染那一步和抓HTML那一步不在同一时刻
  18. 缓存里那份旧HTML,比你想的活得久
  19. 还有一批副本你根本管不到
  20. 还有一类更隐蔽:监控看不见
  21. 一年前用过的地址,今天还剩几条在用?
  22. 按平台看,谁最容易把旧地址弄丢?
  23. 自己站上该怎么办?
  24. 先量一遍,别猜
  25. 把保留期定下来,而不是让它等于零
  26. 清缓存和清文件的顺序别反了
  27. 什么情况下可以不管
  28. 常见问题解答
  29. 旧的静态资源到底该保留多久?
  30. 用内容哈希给文件命名是不是错了?
  31. 资源404会直接影响排名吗?
  32. 怎么知道自己站上有没有这个问题?
  33. 为什么平台CDN上的资源一条都没少?
  34. Next.js这类框架站是不是天生更糟?
  35. 存档站的快照能代表当时的真实页面吗?
  36. 权威参考资料
摘要:从存档里取回125个海外品牌站2025年8月那一天的首页原件,把页面上写着的资源地址逐条拿到今天来敲门:整体只有68.7%还取得回来。同一批站今天的首页引用今天的资源,同样的敲法是98.5%。掉的那部分几乎全在自己域名底下——平台CDN上的74条一条没少,第三方95.7%,而第一方的1452条里有457条已经是干脆的404。最不合直觉的一条:为长缓存特意加了内容指纹的地址,一年后只剩44.5%;反倒是什么版本号都没加的裸文件名活下来77.7%。

前阵子有个做户外装备的客户找过来,说了件怪事:Search Console里那个页面明明索引着,抓取诊断也是200,可Google渲染出来的截图是半张白纸。保哥翻了两天才想明白——渲染那一步去拉的两个JS文件已经不在了。页面本身没动,动的是它引用的东西。

这件事背后有个很朴素的道理:你上线一个新版本,改的只是服务器上那一份HTML;而昨天、上周、去年发出去的那些HTML副本,还留在别人手里。它们躺在CDN的边缘节点上,躺在用户浏览器的缓存里,躺在存档站的快照里,躺在某个AI抓取器的库里。这些副本会照着当时写下的地址,去要那些文件。而那些文件,你早就在下一次构建时清掉了。

那问题就变得可以量了:一份一年前的页面,今天还能把自己拼起来吗?

这个实验是怎么做的,为什么必须先量今天?

样本还是那131个海外品牌站——都是有真实交易的成熟站。做法分三步。

第一步:从存档里取原件

Wayback Machine有一个专门的取法,在时间戳后面加上id_这个后缀,拿回来的是当年那份未经改写的原始HTML。这一点很关键:普通的存档页面会把里面的资源地址全部重写成存档站自己的地址,那样就白测了。带上id_,页面里写的还是当年的原地址。

目标时间点定在2025年8月1日,离现在约13个月。128个站取到了2025年那一带的快照,其中106个正好落在8月、19个落在7月。

第二步:把当年页面上的地址逐条拿到今天来请求

从每份HTML里抽出script标签的src、样式表的href、预加载声明、图片和图标,按注册域分成三类:第一方(跟站点同一个注册域)、平台CDN(不是站点的域,但装的确实是这个站的构建产物,比如cdn.shopify.com)、第三方。每个站探最多14条第一方加4条其它,记状态码、体积、内容类型和最终地址。

第三步:先量今天,再量去年

这一步是整个实验的地基。如果我不先证明今天的页面引用今天的资源能拿满分,那么去年那批的低存活率,就分不清是文件真没了,还是我的抓法本身有问题。所以同一套代码、同一条线路、同一个时刻,对今天的首页再跑一遍,得到的对照组是111个站、1691条地址。

顺带交代一处返工:Wayback在目标时间点附近没有快照时,会自作主张跳到最近的任意一份。有两个站中招——quince落到了2017年12月,rab.equipment落到了2025年12月。这两个站的资源当然一条都不剩,但那说明不了13个月这个口径,所以从实验组里剔掉了,最终样本125个站。

一年前那份首页,今天还能跑起来吗?

两个数摆在一起就够说明问题了:

这批地址来自请求条数今天还取得到存活率
今天的首页(对照组,111个站)1691166698.5%
2025年8月的首页(125个站)1876128868.7%

差了将近30个百分点。对照组那98.5%说明抓法本身没毛病——今天的页面要什么,今天基本都能拿到。

但平均数掩盖了两头

站级的存活率中位数是88.9%,看着还行,可四分位分别是38.9%和100%——这个分布是撕开的,不是聚在中间的。125个站里,57个一条都没掉,37个掉了一半以上,brompton那18条一条都没取到。

换句话说,这件事在多数站上不发生,一旦发生就是成片发生。它不是慢慢腐烂,是一次部署清一批。

掉的是谁的文件?

这是本轮最干净的一组归因。把同一批地址按归属拆开:

归属2025年8月那份页面今天那份页面
平台CDN(cdn.shopify.com这类)74 / 74=100%135 / 135=100%
第三方(统计、客服、广告脚本)335 / 350=95.7%244 / 245=99.6%
第一方(你自己的域名)879 / 1452=60.5%1287 / 1311=98.2%

掉的几乎全是你自己那一部分。平台CDN上的74条,一年之后一条都没少;连那些你根本管不着、天天自己发版的第三方脚本,也还留着95.7%。唯独你自己域名底下的文件,少了将近四成。

把口径再收紧一点

60.5%这个数里混着一些判不了的情况。第一方那1452条的实际状态是:还在879条,返回404或410的457条(31.5%),被403、418这类拒绝挡回来的115条(7.9%)。

后面那115条不能算作文件没了——它们来自13个站,其中boohoo、swarovski、underarmour、warbyparker四家是14条全被拒,明摆着是整站拒绝我这个客户端,跟文件在不在没关系。所以写结论时用31.5%这个保守口径:一年前那份页面上的第一方资源地址,每三条就有一条今天回的是干脆的404。

顺便说,这批站的第三方依赖会自己改版本、改行为,这件事之前两天半就有四分之一的站悄悄变了脚本那轮量过;有意思的是它们改归改,旧地址倒是留着的。

按文件类型再切一刀,掉得最狠的是样式表

把第一方那批地址按类型分开看,顺序有点出人意料:

  • 样式表:57 / 126=45.2%
  • 脚本:585 / 1031=56.7%
  • 图标:16 / 22=72.7%
  • 图片:182 / 226=80.5%
  • 字体:39 / 47=83.0%

样式表和脚本垫底,图片和字体活得好,这个排序不是巧合。越是参与构建流程的文件,越会跟着构建一起换名字;越是被当成素材上传、然后就放在那儿不动的文件,越容易活下来。字体尤其典型,多数团队上传一次就再也不碰,所以它的83.0%基本等于目录本身没被清过。

顺序也决定了后果的轻重。图片掉几张,页面丑一点,内容还在;样式表是阻塞渲染的那一类,掉了之后整页排版散架,渲染截图里几乎无法辨认;脚本掉了则要看它管什么,管交互的影响体验,管内容渲染的直接让页面变空。偏偏掉得最狠的就是后面这两类。

同一个域名下,为什么两个目录的命不一样?

光看第一方和平台CDN的差距还不够硬,因为两者不同域、不同网络路径。真正能定案的是站内对照——同一个域名、同一次请求、同一条线路,只有目录不同。

Shopify站正好提供了这个条件。按它的主题架构,站上的资源分两拨:/cdn/shopifycloud/底下是平台自己发的脚本,/cdn/shop/t/<主题ID>/assets/底下是建站方自己的主题文件。两拨都挂在同一个域名上。

同一批站(45个)的两个目录今天还取得到
/cdn/shopifycloud/ 平台发的198 / 198=100%
/cdn/shop/t/ 主题目录里的149 / 301=49.5%

逐个站比更狠:45个站里,主题目录更差的25个,打平的20个,主题目录更好的一个都没有。其中allbirds、casper、glossier、brooklinen等12个站,是主题目录里探到的文件全部404、而平台目录满分。

这条对照排掉了所有外部干扰。差别只剩一件事:这个目录归谁管。平台把历史版本留着,因为它得对所有商家的所有旧页面负责;主题目录归商家自己,商家改一次主题,上一版的资源就跟着走了。

Next.js那批更极端

另一组站内对照来自/_next/static/。有10个站的样本里同时有这个目录和其它第一方资源:

  • /_next/static/底下:8 / 117=6.8%
  • 同一批站的其它第一方资源:16 / 23=69.6%

6.8%这个数意味着,一年前那份页面上的Next.js构建产物,今天基本上一条都拉不到了。原因不难猜:这个目录里的文件名全部带着构建产生的哈希,每次部署整批改名,旧的既不会保留、也没人再引用。用这类框架做站的人对这套机制通常很熟,但很少有人想过旧页面那一侧会怎样。

给缓存加的那串指纹,为什么反而让文件更早消失?

这是本轮最出乎我意料的一段。把第一方的地址按命名方式分三类:带内容指纹的(文件名里嵌了一串哈希)、查询串带版本号的(后面挂个?v=)、什么都不带的裸文件名。

命名方式2025年8月那份页面今天那份页面
带内容指纹265 / 595=44.5%504 / 515=97.9%
查询串带版本号210 / 337=62.3%348 / 349=99.7%
裸文件名404 / 520=77.7%435 / 447=97.3%

顺序完全反了过来:越是为了缓存做得讲究的命名,一年后越取不到。这个结论要成立还得排掉一种可能——会不会只是因为讲究的团队恰好改版更勤?所以再做一次站内对照:只看那些同时用了两种命名的78个站。

结果一样。带指纹的196 / 393=49.9%,裸文件名293 / 338=86.7%;逐站比,带指纹的更差的26个站,打平的48个,更好的只有4个。

机制其实很直白

内容指纹这套做法的初衷是用文件内容算出一串哈希塞进文件名,这样文件内容一变,地址就变,于是可以给它配一个一年起步的强缓存。web.dev讲长缓存的那篇文章把这套路径说得很清楚。

问题在后半段:地址变了,旧地址就成了没人引用的孤儿,而构建流程默认只发布这一次编译出来的产物。裸文件名反倒因为地址永远不变,只要文件还在原位就一直取得到。

换句话说,指纹命名把资源的生命周期从“跟着文件走”改成了“跟着这一次构建走”。它解决的是缓存不刷新的问题,代价是让每一次部署都作废一整批地址。这个代价从来没写在方案里,因为写方案的人只看新页面。

那些取不到的,究竟是什么形态?

把588条没拿到的逐个看状态码:

  • 404:462条(占绝大多数)
  • 403:102条
  • 418:14条(这个状态码通常是反爬机制在装傻)
  • 429、204、500、400:合计9条

值得单独说的是一个零:没有任何一条被重定向到首页去。这批站在处理不存在的静态文件时相当规矩,回的都是干干净净的404,而不是那种把你送回首页、还硬给个200的软404。只有2条声明是脚本、今天回的却是HTML。

404不是零星漏掉,是整批一起没

把404按站聚起来看,形状很清楚:125个站里,69个站一条404都没有;而另一头,brompton、eufy、everlane、innisfree、on、rapha.cc、ruggable、traeger、vaude、wusthof这10个站,是探到的14条全部404。

中间那一档很薄。这说明它不是文件被一个一个删掉的,而是某一次部署把整个目录换掉了——旧目录连同里面所有东西一起消失。也正因如此,这件事的排查成本其实很低:随便探三五条就能判断,不需要全量扫。反过来,一旦出现,也不会只影响一两个功能。

这对排查是个好消息。死链要不要修得分诊,而这一类死链的特征很好认:状态码干脆、集中在少数几个目录、而且一个站要么不掉要么成片掉。爬虫工具报的死链和Googlebot抓到的常常不是一批,但资源类死链是少数两边都看得见的。

这件事什么时候真的会伤到你?

前面全是数字,这一节说它落到哪儿。

渲染那一步和抓HTML那一步不在同一时刻

Googlebot抓HTML和渲染页面是两个阶段,中间可能隔几小时也可能隔几天。Google关于JavaScript SEO的文档把这个流程画得很明白。如果你在这个间隙里发了一版,渲染队列里那份HTML要的文件就可能已经不在了。对于内容主要靠脚本渲染出来的页面,结果就是索引里留下一个空壳——首页在HTML里本来就是空壳的那批站,风险要再翻一倍。

缓存里那份旧HTML,比你想的活得久

页面本身的缓存时间和资源的保留时间是两笔账,而这两笔账几乎没有人对齐过。之前测缓存指令的受众作用域时见过一个站,页面上写着每次都要回源,CDN手上那份副本却已经躺了9个多小时。缓存碎片还会让不同地区的节点各自留着不同时间点的版本。只要有一个节点上的HTML比你的资源保留期更老,那个节点服务的所有用户就会拿到一份拼不起来的页面。

还有一批副本你根本管不到

存档站、AI抓取器的语料库、别人做的镜像和转存、用户收藏夹里那份浏览器缓存——这些副本会一直照着旧地址去要文件。这次实验用的正是其中一种。做GEO技术优化的时候尤其要留意:机器看到的那一版取决于它取得到哪些文件,而它可不会因为拉不到脚本就回头再来一次。

还有一类更隐蔽:监控看不见

这类故障在服务端日志里表现为一批404,可它们通常混在爬虫瞎试的噪音里,没人当回事。前端监控那边就更麻烦了——跨域资源在性能监控里常常是一排零,拉不到的那些自然也统计不出来。服务器全程200、用户眼前一片空白说的也是这种形态:所有指标都好看,只有用户看到的东西是坏的。

一年前用过的地址,今天还剩几条在用?

换个角度看同一件事。把两份HTML里的资源地址做交集,能看出一年里换掉了多少。

111个有对照的站,一年前和今天都在引用的地址中位数只有9条;而一年前用过、今天已经不再引用的地址中位数是63条,全样本合计7758条。另有11个站一条都没重合——这一年里首页引用的资源被整批换过。

顺带一个规模的量级:这125个站2025年8月那天的首页,一共引用了10119条资源地址,其中第一方6938条。按31.5%的确凿消失率折算,这批页面里大约有2183条地址今天已经敲不开门了。

按平台看,谁最容易把旧地址弄丢?

建站方式站数2025年8月那批地址今天的存活率
Next.js自建632.7%
BigCommerce255.6%
Shopify6571.7%
认不出2677.8%
Salesforce Commerce1090.2%

后面几档的样本量小,只能当参考。但两头的差距足够明显,而且和前面的机制对得上:构建产物越是全量带哈希、部署越是整目录替换,旧地址就死得越彻底。Salesforce那一档之所以高,是因为它的资源路径里带的是版本目录而不是每文件哈希,改版时旧目录往往留着。

这不是平台好坏的排序。选平台该看的东西多着呢,回源策略与缓存键怎么设计这些都得一起算,本文只是补上以前没人拿出来量过的一项成本。

自己站上该怎么办?

先量一遍,别猜

不需要什么工具。找一份你自己站三个月前、半年前、一年前的存档快照,用带id_的地址取回原件,把里面的第一方资源地址抓出来,逐条请求一遍。保哥这一轮125个站跑下来也就一顿饭的工夫,单站半小时都用不上,得到的是你自己那条曲线。

关键是一定要同时跑今天那一份做对照。没有这个零点,任何数字都不知道该怎么读。

如果懒得取存档,还有一个更省事的替代:翻服务器日志,把静态目录下返回404的请求捞出来,只看那些引用来源是本站页面的。referer里写着自己站的404,几乎一定是旧HTML在要旧文件——没有别人会凭空猜出一个带哈希的文件名。这批记录的量和分布,就是你这套保留期问题的实时读数。

把保留期定下来,而不是让它等于零

多数团队的构建流程是清空目录再发布,等于把旧版本的保留期定成了零,而且是无意识的。定一个明确的值更好:

  • 下限看你的页面缓存活多久。页面的max-age、CDN的边缘缓存时间、浏览器缓存策略——取里面最长的那个,旧资源至少得活过它。
  • 再加上渲染队列的滞后。抓取和渲染之间那段时间要算进去,留几天是稳妥的。
  • 用发布次数兜底也行。保留最近若干次部署的产物,比按天数更贴合发布节奏快的团队。

Next.js有一个叫deploymentId的配置,Vercel那边叫作偏斜保护,做的正是这件事:让旧页面发出的请求能被路由回它当初那一版。这两份文档(deploymentIdSkew Protection)值得部署前读一遍。自建的话,最省事的做法是把构建产物按版本号放进不同目录,然后只清理超过保留期的那些。

清缓存和清文件的顺序别反了

删旧资源之前,先把页面缓存清干净,让新HTML铺开,再等一个宽限期动手。反过来做——先删文件再清缓存——中间那段时间里,所有拿到旧HTML的人都会撞上404。清缓存这件事本身要留回执,不然你不知道到底清没清干净。

什么情况下可以不管

这件事不是所有站都要处理。判据有三条,都不占的话,把精力放别处更划算:

  1. 页面的主要内容需要脚本渲染才出得来。纯服务端渲染、脚本只管交互的站,掉几个文件不影响内容被读到。
  2. 发布频率高,或者页面缓存时间长。两者只要占一样,旧HTML和新资源对不上的窗口就一直开着。
  3. 资源命名用了内容指纹。这次实测里它的一年存活率只有44.5%,是三种命名方式里最低的一档。

说到底,这件事的性质和站点上很多东西一样:preconnect指着一个已经不存在的域名三个月前抄下来的那串完整性哈希拦住了新版脚本,都是同一个毛病的不同长相——页面和它依赖的东西各走各的时间线,而没有人在负责让这两条线对齐。

常见问题解答

旧的静态资源到底该保留多久?

取三个数里最大的那个:页面本身的缓存时间(包括CDN边缘节点的)、搜索引擎抓取到渲染之间的滞后、以及你的发布间隔。多数电商站落在两周到一个月这个区间是稳妥的。如果按次数管理更顺手,保留最近10到20次部署的产物也是一样的效果。

用内容哈希给文件命名是不是错了?

不是。哈希命名解决的是缓存不刷新这个真问题,该用还是要用。这次实测揭示的是它的另一面代价:地址一变,旧地址就没人引用了,而构建流程默认不会替你保留。正确的做法是继续用哈希命名,同时给旧产物设一个明确的保留期,两件事不冲突。

资源404会直接影响排名吗?

404这个状态码本身不是排名信号。真正的影响走的是另一条路:如果页面内容靠脚本渲染,而渲染时拉不到脚本,Google索引里存的就是一个空壳;如果掉的是CSS,页面在渲染截图里可能被判成排版异常。掉几张装饰图片则基本没有影响。

怎么知道自己站上有没有这个问题?

最直接的办法是从存档站取一份自己半年前的首页原件,把里面的第一方资源地址逐条请求一遍,同时对今天的首页做同样的事作为对照。也可以看服务器日志里静态目录的404,特别注意那些引用来源是本站页面的记录,那说明有人正拿着旧HTML在访问。

为什么平台CDN上的资源一条都没少?

因为平台要对所有商家的所有历史页面负责,删掉一个旧版本会同时打断很多店铺。它的成本模型允许长期保留,而单个商家的构建流程默认是清空重发。这次实测里,同一批Shopify站上,平台目录的存活率是100%,商家自己的主题目录是49.5%,逐站比对没有一个站的主题目录更好。

Next.js这类框架站是不是天生更糟?

不是天生,是默认配置带来的。这类框架的构建产物全量带哈希、部署时整目录替换,所以旧地址消失得最彻底——实测里带这个目录的10个站,旧产物只剩6.8%。框架自己提供了应对手段,Next.js的deploymentId和Vercel的偏斜保护都是干这个的,只是默认不开。

存档站的快照能代表当时的真实页面吗?

取原件时带上id_后缀,拿到的就是当年抓取时服务器返回的原始HTML,里面的资源地址没有被改写过。要留意的是存档站可能在目标日期附近没有快照,会跳到最近的任意一份——这次就有两个站落到了2017年和2025年12月,必须挑出来剔掉,否则口径会被污染。

权威参考资料

分享到
标签
版权声明

本文标题:《静态资源带上哈希之后,一年后一半地址都404了》

本文链接:https://zhangwenbao.com/static-asset-one-year-survival-404-audit.html

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

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