静态资源带上哈希之后,一年后一半地址都404了
本文目录
- 这个实验是怎么做的,为什么必须先量今天?
- 第一步:从存档里取原件
- 第二步:把当年页面上的地址逐条拿到今天来请求
- 第三步:先量今天,再量去年
- 一年前那份首页,今天还能跑起来吗?
- 但平均数掩盖了两头
- 掉的是谁的文件?
- 把口径再收紧一点
- 按文件类型再切一刀,掉得最狠的是样式表
- 同一个域名下,为什么两个目录的命不一样?
- Next.js那批更极端
- 给缓存加的那串指纹,为什么反而让文件更早消失?
- 机制其实很直白
- 那些取不到的,究竟是什么形态?
- 404不是零星漏掉,是整批一起没
- 这件事什么时候真的会伤到你?
- 渲染那一步和抓HTML那一步不在同一时刻
- 缓存里那份旧HTML,比你想的活得久
- 还有一批副本你根本管不到
- 还有一类更隐蔽:监控看不见
- 一年前用过的地址,今天还剩几条在用?
- 按平台看,谁最容易把旧地址弄丢?
- 自己站上该怎么办?
- 先量一遍,别猜
- 把保留期定下来,而不是让它等于零
- 清缓存和清文件的顺序别反了
- 什么情况下可以不管
- 常见问题解答
- 旧的静态资源到底该保留多久?
- 用内容哈希给文件命名是不是错了?
- 资源404会直接影响排名吗?
- 怎么知道自己站上有没有这个问题?
- 为什么平台CDN上的资源一条都没少?
- Next.js这类框架站是不是天生更糟?
- 存档站的快照能代表当时的真实页面吗?
- 权威参考资料
摘要:从存档里取回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个站) | 1691 | 1666 | 98.5% |
| 2025年8月的首页(125个站) | 1876 | 1288 | 68.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自建 | 6 | 32.7% |
| BigCommerce | 2 | 55.6% |
| Shopify | 65 | 71.7% |
| 认不出 | 26 | 77.8% |
| Salesforce Commerce | 10 | 90.2% |
后面几档的样本量小,只能当参考。但两头的差距足够明显,而且和前面的机制对得上:构建产物越是全量带哈希、部署越是整目录替换,旧地址就死得越彻底。Salesforce那一档之所以高,是因为它的资源路径里带的是版本目录而不是每文件哈希,改版时旧目录往往留着。
这不是平台好坏的排序。选平台该看的东西多着呢,回源策略与缓存键怎么设计这些都得一起算,本文只是补上以前没人拿出来量过的一项成本。
自己站上该怎么办?
先量一遍,别猜
不需要什么工具。找一份你自己站三个月前、半年前、一年前的存档快照,用带id_的地址取回原件,把里面的第一方资源地址抓出来,逐条请求一遍。保哥这一轮125个站跑下来也就一顿饭的工夫,单站半小时都用不上,得到的是你自己那条曲线。
关键是一定要同时跑今天那一份做对照。没有这个零点,任何数字都不知道该怎么读。
如果懒得取存档,还有一个更省事的替代:翻服务器日志,把静态目录下返回404的请求捞出来,只看那些引用来源是本站页面的。referer里写着自己站的404,几乎一定是旧HTML在要旧文件——没有别人会凭空猜出一个带哈希的文件名。这批记录的量和分布,就是你这套保留期问题的实时读数。
把保留期定下来,而不是让它等于零
多数团队的构建流程是清空目录再发布,等于把旧版本的保留期定成了零,而且是无意识的。定一个明确的值更好:
- 下限看你的页面缓存活多久。页面的max-age、CDN的边缘缓存时间、浏览器缓存策略——取里面最长的那个,旧资源至少得活过它。
- 再加上渲染队列的滞后。抓取和渲染之间那段时间要算进去,留几天是稳妥的。
- 用发布次数兜底也行。保留最近若干次部署的产物,比按天数更贴合发布节奏快的团队。
Next.js有一个叫deploymentId的配置,Vercel那边叫作偏斜保护,做的正是这件事:让旧页面发出的请求能被路由回它当初那一版。这两份文档(deploymentId与Skew Protection)值得部署前读一遍。自建的话,最省事的做法是把构建产物按版本号放进不同目录,然后只清理超过保留期的那些。
清缓存和清文件的顺序别反了
删旧资源之前,先把页面缓存清干净,让新HTML铺开,再等一个宽限期动手。反过来做——先删文件再清缓存——中间那段时间里,所有拿到旧HTML的人都会撞上404。清缓存这件事本身要留回执,不然你不知道到底清没清干净。
什么情况下可以不管
这件事不是所有站都要处理。判据有三条,都不占的话,把精力放别处更划算:
- 页面的主要内容需要脚本渲染才出得来。纯服务端渲染、脚本只管交互的站,掉几个文件不影响内容被读到。
- 发布频率高,或者页面缓存时间长。两者只要占一样,旧HTML和新资源对不上的窗口就一直开着。
- 资源命名用了内容指纹。这次实测里它的一年存活率只有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