做SEO盯了三年网站速度,页面上近一半的资源在监控里是一排零

做SEO盯了三年网站速度,页面上近一半的资源在监控里是一排零
张文保 更新 37 分钟阅读 2,733 阅读
本文目录
  1. 网站速度做SEO,为什么报表上那个数是真的、定位却一步也走不下去?
  2. 那条分界线画在哪里
  3. 同源这条线,比大多数人以为的严
  4. 先看一眼这件事在真实站点上有多普遍
  5. 跨源资源上,到底哪几个字段被抹成了零?
  6. 同源和跨源,同一张图的两条记录
  7. 剩下的那几个字段,恰好是最没用的那几个
  8. 零这个值,最坏的地方在于它像个测量结果
  9. 已经配了跨域许可,为什么时间还是读不到?
  10. 单变量把两把钥匙拆开
  11. 现网360个资源只配了其中一把
  12. 顺带一个小发现:有9个资源把这行头发了两遍
  13. 同一张1.6 MB的图,为什么两个浏览器报出的字节数差了三个数量级?
  14. 300不是错误,是规范里的一个常量
  15. 这件事会怎么落到你的报表上
  16. 一条跳转链上少发一次这行头,会毁掉多少?
  17. 为什么这条特别容易踩
  18. 服务端埋好的耗时明细,为什么一条都没到前端?
  19. 最大内容绘制说得出是哪张图,为什么说不出它慢在哪一步?
  20. 两张表,一张什么都知道,一张什么都不知道
  21. 为什么这一层对做SEO的人特别难受
  22. 关于渲染时刻,有个说法已经过时了
  23. 顺手撞见的一条:两个引擎认的最大元素不是同一个
  24. 88个真实站点的首页,这一项到底配到了什么程度?
  25. 先看总量
  26. 再看按资源类型拆
  27. 最后看按托管方拆——这一栏解释了前面所有数字
  28. 这一项该怎么补?补不了的时候还能怎么测?
  29. 自有服务器上就是一行
  30. 写精确来源时,这四种写法全部静默失效
  31. 资源在别人手里的时候
  32. 第三档怎么办:换四个口径接着测
  33. 先花两分钟,把自己站上的盲区列出来
  34. 一份能直接抄走的检查清单
  35. 常见问题解答
  36. 这行头会不会带来安全或者隐私风险?
  37. 配了它,跨域问题是不是也一起解决了?
  38. 为什么控制台不报警告,只给一排零?
  39. 这件事会直接影响排名吗?
  40. 那些第三方挂件的时间读不到,还有别的办法定位吗?
  41. 只有一半的跨源资源配了这个,是不是说明它没那么重要?
  42. 权威参考资料

摘要:网站速度是Google明说在用的排名信号,于是大家都装了监控。但浏览器交给你的那张资源明细表,对跨源资源默认是一排零。保哥搭了一台真实HTTPS三主机名实验台,把38种写法在两个浏览器引擎上各跑一遍,又扫了88个能抓到首页的线上站点:跨源资源占了全部资源引用的48.9%,其中只有49.6%发了那个决定你能不能看见时间的响应头。更麻烦的是,读不到的时候它不报错、不告警、不留空,它给你一个0,而0长得跟测量结果一模一样。

先说一个很多人都遇到过的场景。

季度体检报告出来了,首页的最大内容绘制3.4秒,红的。老板问怎么回事,你说我查。

打开监控面板,那个3.4秒是真的,多个来源都对得上。往下钻,想知道这3.4秒里有多少花在解析域名、多少花在握手、多少花在等服务端第一个字节、多少花在把图片传完。

面板给你的是这样一行:

domainLookupStart: 0
connectStart:      0
requestStart:      0
responseStart:     0
transferSize:      0

五个零。

你的第一反应大概是缓存命中了,或者埋点写错了,或者这个字段在这个浏览器上没实现。三种猜测都很合理,也都不对。

真正的原因是:那张图片挂在另一个域名下,而那个域名没有在响应里发一行叫Timing-Allow-Origin的头。浏览器于是按规矩把这条记录里的大部分字段抹成了零,一句话也没说。

网站速度做SEO,为什么报表上那个数是真的、定位却一步也走不下去?

先把这件事的分量说清楚。

Google在页面体验那份文档里写得没有余地:Core Web Vitals are used by our ranking systems.同一份文档也补了另一句,There is no single signal.两句话合起来的意思是,速度进了排名,但它不是一个开关,而是一堆你得逐项去改的东西。

要逐项去改,前提是能逐项测出来。而页面速度影响排名的那套优先级里最耗人的一步从来不是改,是定位——先得知道那3.4秒到底卡在谁身上。

浏览器为这件事准备了一张表,叫资源计时。页面上每加载一个东西,它就记一条,从什么时候开始查域名,到什么时候最后一个字节到齐,中间十几个时间点全给你。

这张表非常好用。前提是那个资源跟页面同源。

那条分界线画在哪里

分界线就是同源与否,判据是协议、主机名、端口三样必须完全一样。

这个判据听着很技术,落到实际站点上就是一句大白话:凡是别人替你托管的东西,都在线外。

图片放在对象存储或图床上,在线外。脚本走公共CDN,在线外。字体从字体服务拉,在线外。统计代码、客服挂件、评价挂件、支付按钮、同意管理弹窗、A/B测试脚本,全部在线外。

这就有点尴尬了。因为过去十年前端提速的标准打法,恰恰就是把静态资源全搬到CDN上去。CDN那六层缓存与边缘路由确实把首字节时间压下来了,代价是把这些资源一起挪出了你的可观测范围。

你为了让页面变快做的那件事,同时让你看不见它到底变快了多少。这不是谁的疏忽,这是两个规范各自成立、碰在一起的结果。

同源这条线,比大多数人以为的严

这里容易出一类误判,值得单独拎出来。

很多人默认“自家域名下的东西都算自己人”,但同源的判据是协议、主机名、端口三样逐字相同,任何一样不一样都算跨源。落到实际配置上:

页面地址资源地址算不算同源
https://shop.com/https://img.shop.com/a.jpg跨源
https://shop.com/https://www.shop.com/a.jpg跨源
https://shop.com/http://shop.com/a.jpg跨源
https://shop.com/https://shop.com:8443/a.jpg跨源
https://shop.com/https://shop.com/img/a.jpg同源

第二行是最常见的那个坑。带不带三个w,在很多人的心智里是同一个站,在这条判据下不是。多域名统一跳主域那套配置通常只管页面地址,图片、脚本这些资源如果历史上写的是另一个写法,它们就一直待在线外。

第四行同理。测试环境、灰度环境、内部工具挂在非标准端口上,跟主站是跨源关系。

先看一眼这件事在真实站点上有多普遍

保哥拿164个线上站点的首页跑了一轮,能正常抓到并解析的有88个。把每个首页上会进入资源计时的标签全抽出来,按主机名分成同源和跨源两堆:

口径数字
抓到并解析的首页88个
首页上有跨源资源的站80个
全部资源引用中同源的3108条
全部资源引用中跨源的2978条
跨源占比48.9%
单站跨源资源数中位数14.5个
单站跨源主机数中位数3个

差不多一半。

也就是说,默认状态下,你那张资源明细表有一半的行是空的。而这一半里,装着首屏大图、装着阻塞渲染的样式表、装着那个每次都被怀疑拖慢页面的第三方脚本。这跟服务器配置里那些影响SEO的项不太一样:那些项配错了会有症状,这一项配没配,页面上一点区别都没有。

跨源资源上,到底哪几个字段被抹成了零?

光说“读不到”太含糊。实验台的价值就在这里:把每一个字段挨个看一遍,看清楚被拿走的到底是什么。

实验台是三个主机名跑在同一个真实HTTPS服务上:一个放页面,一个当第三方资源源,第三个专门做跳转链的终点。用例定义写在服务端,跑用例的程序从服务端拉清单,所以名字不可能对错。每个用例只改一个变量。

同源和跨源,同一张图的两条记录

同一张40像素见方的PNG,同一台服务器,只换主机名:

字段同源跨源且没发那行头
startTime18.13.2
fetchStart18.13.2
domainLookupStart18.10
domainLookupEnd18.10
connectStart18.10
connectEnd18.10
secureConnectionStart18.10
requestStart18.60
responseStart18.90
responseEnd19.118.3
duration115.1
transferSize4060
encodedBodySize1060
decodedBodySize1060
nextHopProtocolhttp/1.1空字符串
responseStatus2000

两个引擎在这张表上的判决完全一致。

资源计时规范里对这件事的措辞是“不透明条目”。它没有含糊其辞,而是把被抹掉的字段一个个点了名:

原文是the following attributes will always return zero or the empty string,后面跟着这份名单:

redirectStart              redirectEnd
workerStart                domainLookupStart
domainLookupEnd            connectStart
connectEnd                 requestStart
firstInterimResponseStart  finalResponseHeadersStart
responseStart              secureConnectionStart
nextHopProtocol

实测结果跟这份名单逐字对得上。

剩下的那几个字段,恰好是最没用的那几个

没被抹掉的只有四个:开始时间、开始取数的时间、整体结束时间、总时长。

换成人话:你知道它几点开始、几点结束、一共花了多久,但中间查域名、握手、加密协商、等待服务端、传输字节这五段,一段都看不到。

这就是全部问题所在。总时长这个数,本来就是监控面板早就告诉过你的。你钻进这张表,要的从来就是那五段。

而且请注意duration这一列:同源那次是1毫秒,跨源那次是15.1毫秒。同一台服务器上的同一张图片,跨源那次看起来慢了十几倍。这个差异跟安全策略无关,是本地解析新主机名带来的,但如果你只盯着duration做归因,很容易得出一个完全反向的结论。

零这个值,最坏的地方在于它像个测量结果

如果读不到的时候浏览器给的是null,或者干脆不给这个字段,事情会好办很多——你的代码会立刻炸,或者面板上会出现一个明显的空洞。

给的是0。

0会被求平均,会被画进折线图,会被写进周报。一个站有40%的图片来自图床,那这个站的“平均握手耗时”里就掺了40%的零,算出来的数字会显著偏低,而且低得非常稳定、非常好看。

一项测量失败,如果失败值恰好落在合法取值范围内,它就不再表现为故障,而是表现为一个偏乐观的结论。这比直接报错难查得多。

这跟搜索后台那几个数据黑洞是同一类毛病:数据不是没给,是给了一个经过处理、而处理规则没写在面板上的版本。

已经配了跨域许可,为什么时间还是读不到?

这是现网最普遍的一个误解,也是这次普查里数字最刺眼的一条。

很多人的印象是:跨域的事情,配了Access-Control-Allow-Origin就通了。字体能加载了,接口能调了,画布不脏了,那时间自然也该能读了。

不是的。这里有两把钥匙,开的是两扇不同的门。

单变量把两把钥匙拆开

实验台上四个用例,每次只改一个变量:

用例跨域许可计时许可五段时间responseStatus
只配跨域许可全零0
跨域许可加上标签属性全零200
只配计时许可全开0
两个都配全开200

两个引擎判决一致。

看第二行和第三行。第二行把跨域许可配得很完整——响应头发了,标签上的crossorigin属性也加了——结果时间一个都读不到,倒是把状态码露出来了。第三行反过来,只发计时许可,时间全开,状态码却是0。

所以边界是这样的:

  • 计时许可管时间和字节:那五段时间点、三个字节数、协议名,归Timing-Allow-Origin管。
  • 跨域许可管内容:状态码属于响应内容的一部分,归Access-Control-Allow-Origin管。

MDN给这行响应头下的定义写得其实很清楚:specifies origins that are allowed to see values of attributes retrieved via features of the Resource Timing API, which would otherwise be reported as zero due to cross-origin restrictions.它的主语从头到尾都是资源计时接口,一个字没提跨域资源共享。

现网360个资源只配了其中一把

把这条判据拿去量真实站点,结果比预期的还整齐。1130个探到的跨源资源里:

配置组合数量能读到时间吗
有跨域许可,有计时许可561
有跨域许可,没有计时许可360不能
有计时许可,没有跨域许可0——
两个都没有209不能

倒数第二行是这次普查里保哥最没想到的一个数。

1130个跨源资源,没有一个是单独配了计时许可的。一个都没有。

这说明什么?说明这一项从来就不是被主动配置的,它是跟着别的东西一起被配上的。哪家CDN的默认模板里带了这一行,用它的站就有;没带,用它的站就没有。中间没有人做过决定。

一项配置如果在现网从来没有被单独启用过,那它大概率不在任何人的检查清单上。它的覆盖率不反映需求,只反映默认值。

顺带一个小发现:有9个资源把这行头发了两遍

561个带计时许可的资源里,值的分布是这样的:501个是通配符,50个是一串用逗号分隔的精确来源,1个是单一精确来源。

剩下9个的值是*, *

两个通配符,中间一个逗号。语法上完全合法,效果跟一个通配符没区别。它的来源基本可以肯定:请求路上有两层东西各自加了一次,谁也不知道对方加过。这行头就这么被写了两遍,安安静静地在生产环境上跑着。

同一张1.6 MB的图,为什么两个浏览器报出的字节数差了三个数量级?

这一节的发现完全在计划之外,但它可能是整篇里最该被记住的一条。

实验台上有一张900乘620的噪点图,实际传输1597409字节。把它挂在跨源主机上,配好计时许可,不配跨域许可,两个引擎读出来的字节数是:

配置引擎A transferSize引擎B transferSize引擎B encodedBodySize
同源159740915974091597109
跨源,只有计时许可15974093000
跨源,计时许可加跨域许可159740915974091597109
跨源,什么都没有000

第二行。同一张图,一个引擎说1597409字节,另一个说300字节。

300不是错误,是规范里的一个常量

这个数字有出处。资源计时规范里对传输大小的算法写的是Return this's resource info's encoded size plus 300.那个300是给HTTP响应头留的固定估值。当实体大小拿不到、按0算的时候,这个式子的结果就正好是300。

所以引擎B的行为是严格的:计时许可开了时间,但实体有多大属于响应内容,没有跨域许可就不给,于是实体按0算,加上头部估值300,得到300。引擎A宽松一些,认了计时许可就把字节数一起给了。

规范这一条同时解释了另一个常见的困惑:为什么有些资源的transferSize恰好是300。那不是它真的只传了300字节,那是“我知道有这么个响应,但实体大小我不能告诉你”。

这件事会怎么落到你的报表上

设想一个很常规的需求:统计首页图片总字节数,找出最该压缩的那几张。

代码就一行,把资源计时里initiatorType是图片的条目的transferSize加起来。这段代码跑在真实用户的浏览器上,然后回传。

于是同一批用户访问同一个页面,回传上来的数据分成两拨:一拨报的是1.6 MB,一拨报的是300字节。它们在你的数据库里没有任何标记可以区分,混在一起求平均。

你会看到图片总字节数比预期低很多,然后合理地推断出图片不是瓶颈,把优化精力挪到别处去。

跨引擎差异之所以危险,不在于两边行为不同,而在于两边的数据长得一样、能求平均、还都不带来源标记。一个300和一个1597409放在同一列里,看不出它们回答的根本不是同一个问题。

这跟几家工具数据对不上时该怎么对账是一个道理,区别在于那边至少你知道自己在比两个来源,这边你以为自己只有一个来源。

一条跳转链上少发一次这行头,会毁掉多少?

图片走跳转是很常见的。图床按参数生成不同尺寸,老地址迁到新桶要保兼容,多媒体服务按地区分发要就近落点——这些都会在资源前面加一跳。

实验台上做了一组:图片从第三方源A出发,跳到第三方源B落地,两跳的计时许可分别开关:

第一跳终点redirectStart五段时间字节数
发了发了可读全开可读
没发发了0全零0
发了没发0全零0
没发没发0全零0

两个引擎判决一致。

规则是全票通过制:链上任何一跳没通过,整条记录作废,包括那些明明通过了的跳。

第三行尤其值得看一眼。终点那台机器什么都没做错——它压根没被问到。是前面那一跳先把整条链判死了,后面配得再对也进不了账。

为什么这条特别容易踩

因为跳转那一跳,通常不是资源真正的家。

那可能是一条CDN的规则、一个反向代理、一段迁移期的兼容配置。它只负责把请求转走,返回体是空的,谁也不会想到要给一个空响应配响应头。而反向代理那些末尾斜杠与转发规则的坑本来就多,响应头这一层本来就容易漏配,再叠一层看不见的计时许可,排查起来基本没有线索。

更棘手的是,这个失败在页面上什么都不影响。图片照样显示,功能照样跑,控制台干干净净。唯一的症状是监控面板上那一行是零——而那一行你多半根本没在看。

服务端埋好的耗时明细,为什么一条都没到前端?

还有一个被同一把锁锁住的东西,很多团队花了不少力气做,然后发现前端读不到,最后不了了之。

那就是Server-Timing。服务端把这次请求里查库花了多久、缓存命中没有、模板渲染多久,写进响应头,前端就能在资源计时里读出来。这是把服务端耗时和前端体验串成一条线的最直接的办法。

实验台上三组对照,服务端一律发了两条计时明细:

用例响应头里真的有前端serverTiming读到
同源2条2条
跨源,没发计时许可2条空数组
跨源,发了计时许可2条2条

两个引擎一致。MDN在服务端计时那一页上给的话很干脆:This interface is restricted to the same origin, but you can use the Timing-Allow-Origin header to specify the domains that are allowed to access the server metrics.

注意中间那行的对比:响应头里确确实实躺着两条,用命令行请求一下就能看见,但页面里读出来是一个长度为0的数组。不是报错,不是权限异常,就是空的。

普查里268个跨源资源发了这个头,其中4个没配计时许可。那4个资源上的服务端耗时明细,从上线那天起就没有任何一个前端读到过。这类“做了但没接通”的投入很难被发现,因为它不在任何一张报错清单上——跟日志分析里那些采了却没人读的字段是同一种浪费。

最大内容绘制说得出是哪张图,为什么说不出它慢在哪一步?

到这里可以说最要命的那一层了。

最大内容绘制这个指标,是Core Web Vitals里权重最高、也最常爆红的一项。它的判定对象通常就是首屏那张大图。而首屏大图,恰恰是最容易挂在图床上的东西。

实验台上把一张900乘620的图放成首屏主图,跨源、不发计时许可,然后同时读两张表。

两张表,一张什么都知道,一张什么都不知道

来源字段读到的值
最大内容绘制条目url完整地址
最大内容绘制条目size558000
最大内容绘制条目renderTime68
最大内容绘制条目loadTime36.3
资源计时条目domainLookupStart0
资源计时条目connectStart0
资源计时条目requestStart0
资源计时条目responseStart0
资源计时条目transferSize0

同一张图,同一次加载,两张表。

上半张表把该说的都说了:是这个地址,占了这么大面积,几点画到屏幕上,几点下载完。

下半张表对同一张图片,五段时间加三个字节数,全是零。

指标告诉你“是这张图慢”,而唯一能告诉你“它慢在哪一步”的那张表,正好对这张图什么都不说。这不是监控没做好,这是两个接口各自的可见性规则拼出来的结果。

于是那个季度体检报告的追查就卡在这里:你知道罪魁祸首是哪张图了,接下来该判断是换CDN节点、还是加预连接、还是压缩图片、还是干脆换格式——而这四条路对应的正是那五段时间里的不同段落,你一段都看不见。

为什么这一层对做SEO的人特别难受

因为速度这件事有两套数据,而这个盲区正好卡在两套数据中间。

一套是实验室数据:用工具在受控环境里跑一遍,什么都能看见,包括跨源资源的完整瀑布图——工具是在自己的浏览器实例里跑的,它有的是办法。另一套是字段数据:真实用户访问时采集回来的,决定你体检结果的是这一套。

问题在于,实验室那次跑出来的瓶颈,未必是真实用户遇到的瓶颈。真实用户分布在不同网络、不同地区、不同设备上,跨源资源的表现差异恰恰在这里最大。

于是你想在真实用户那边验证一下实验室的结论——而真实用户那边,正是这一排零所在的地方。

结果就是:能看清的那次不代表大多数人,代表大多数人的那次看不清。做前端与SEO协作时最常见的扯皮,很多都源自这里——两边各拿着一套数据,谁也说服不了谁,而两套数据的采集口径根本不同。

关于渲染时刻,有个说法已经过时了

网上流传比较广的一个说法是:跨源图片的renderTime恒为0,必须用loadTime顶上。

实测两个引擎都不是这样了。跨源不发计时许可的那组,renderTime读到68,同源基线读到44,都不是0。

MDN渲染时刻那一页现在的写法也改了:For security reasons, the value of the renderTime property was originally 0 if the resource is a cross-origin request.注意那个originally。后面紧跟着Browsers may now expose a slightly coarsened render time in these situations.

但它同时给了一条现在依然成立的建议:developers should use startTime over renderTime as the LCP time.理由是startTime会自动在两者之间做回退,不用你自己判断0。

把这条落成一句检查项:如果你的埋点代码里写着判断renderTime是不是0,那段逻辑今天已经不会命中了,但它也没坏,只是变成了一段永远走不到的分支。这类代码是最难被发现的,因为它不报错。

顺手撞见的一条:两个引擎认的最大元素不是同一个

这是做实验时踩出来的。

最早那版实验用的是一张纯色大图,900乘620,压缩后3332字节。结果引擎A死活不把它算成最大内容绘制的对象,最大元素退回到了页面上那行标题文字。引擎B则老老实实把它认成了主体,面积558000。

算一下就明白了:3032字节的实体,除以558000个像素,每像素约0.043比特。这个数低于引擎A那条把低信息量图片排除在候选之外的阈值——那条规则本来是防止有人拿一张巨大的纯色图去刷指标的。

换成噪点图,实体涨到1.59 MB,两个引擎立刻一致认它当主体。

这件事在真实站点上不算罕见。纯色背景块、渐变底图、大面积留白的品牌图、纯色占位图,都可能踩到这条线。后果是:同一个页面,两个引擎报的最大元素是不同的东西,于是报的最大内容绘制时刻也是不同的时刻。而你的报表把它们放在同一个指标名下面。

88个真实站点的首页,这一项到底配到了什么程度?

实验台讲的是机制,普查讲的是这套机制在现实里长什么样。

做法是这样:对每个抓到的首页,把跨源资源的地址全抽出来,逐个发一次请求,只读响应头。一共探到1130个可用的跨源资源。

先看总量

口径数字占比
探到的跨源资源1130——
带计时许可的56149.6%
有跨源资源的站79——
至少有一个跨源资源读不到时间的站6886.1%
跨源资源一个都没配的站2835.4%
跨源资源全配齐的站1113.9%
单站读不到的跨源资源中位数3个——

差不多一半的跨源资源能读到时间。听起来还行,但站点层面的数字才是重点:86.1%的站至少有一个盲点,35.4%的站是全盲。

再看按资源类型拆

标签探到数量带计时许可
link(样式表与预载)38671.2%
img40744.5%
source4639.1%
script23934.3%
video1533.3%
iframe370%

脚本34.3%,图片44.5%。这两类恰好是最需要被归因的——脚本决定主线程什么时候空出来,图片决定最大内容绘制什么时候发生。

嵌入框架37个,一个都没有。评价挂件、客服窗口、地图、视频播放器、支付按钮,基本都是这个形态。它们在你的性能报表里,从来就没有过时间。

最后看按托管方拆——这一栏解释了前面所有数字

资源主机探到数量带计时许可
cdn.shopify.com153100%
github.githubassets.com26100%
b.stripecdn.com22100%
res.cloudinary.com20100%
s.alicdn.com16100%
images.ctfassets.net490%
cdn.prod.website-files.com410%
www.googletagmanager.com330%
cdn.sanity.io250%
a.storyblok.com240%
cdn.cookielaw.org140%
definitions.sqspcdn.com130%

没有中间地带。要么100%,要么0%。

这张表把结论一次说完了:你能不能测到跨源资源的时间,基本不由你决定,由你选的那个平台决定。而选平台那个决定,是在做技术选型的时候顺手做掉的,做的人多半不知道有这一项。

再看那11个全配齐的站是谁:清一色是自己就是平台方的那些——建站平台自己、支付服务商自己、代码托管自己、几家大型跨境电商平台自己。它们的资源就托管在自家CDN上,配置权在自己手里。

而那28个全盲的站,几乎全是用现成内容平台加现成图床搭起来的中小独立站。

同一项能力,在自建方那里是一行配置,在采买方那里是一个连提都提不了的诉求。可观测性的贫富差距,跟基础设施的自有比例是同一条曲线。

还有一个细节值得单独说:标签管理器那33个,覆盖率是0%。这是全世界装机量最大的一类第三方脚本,几乎每个做监控告警体系的站都装了它。而它自己,恰恰是那个你永远量不出加载耗时的东西。挑监控工具的时候不妨多问一句:它报的第三方资源耗时,是真读到的,还是拿总时长凑出来的。

这一项该怎么补?补不了的时候还能怎么测?

先说好消息:能改的地方,改起来是一行的事。

自有服务器上就是一行

如果这批资源在你自己的服务器上,只是挂在另一个域名下(图片站、静态资源域、多语言子域这类),那就直接加:

# Nginx
add_header Timing-Allow-Origin "*" always;

# Apache
Header always set Timing-Allow-Origin "*"

always这个词别省。不加的话,重定向、错误页这类非200响应上不会带这行头,而跳转规则那一层正好是最容易漏的地方——前面那组跳转链的实验已经说明,漏在跳转上跟没配是一个后果。

至于该不该用通配符:这行头暴露的只是时间和字节数,不涉及内容。普查里501个直接用了通配符,就是这个道理。如果确实想收紧,写精确来源也行,但接下来那几个坑要看清楚。

写精确来源时,这四种写法全部静默失效

实验台上专门测了一组常见手滑,两个引擎判决完全一致:

写法结果
通配符生效
精确来源,逐字对上生效
逗号分隔的多个来源,其中一个对上生效
精确来源,末尾多一个斜杠失效
精确来源,主机名写成大写失效
精确来源,漏写端口号失效
这行头发了,但值是空字符串失效
值写成null失效

四种手滑,四种失效,零个提示。

失效的表现跟压根没配一模一样:那条记录变成一排零,控制台不吭声,页面一切正常。你唯一能确认它没生效的办法,是自己去读那条资源计时记录,看requestStart是不是0。

末尾斜杠那条最坑。因为在几乎所有别的地方,https://a.comhttps://a.com/都是一回事,运维配的时候顺手带个斜杠是很自然的动作。这里不是。

一个只做逐字比对、失败后静默降级的配置项,它的真实错误率跟它的语法宽容度成反比。越是没人校验的字段,越应该用通配符,而不是精确值。

资源在别人手里的时候

这才是大多数人的处境。按普查那张按托管方拆的表,情况分三档。

第一档,平台默认就配了。建站平台自家CDN、几家主流图片处理服务、大型支付服务商的静态资源,实测都是100%。这一档你什么都不用做。

第二档,平台支持自定义响应头,藏在控制台某个不显眼的地方。可编程边缘那一层通常都能加,在边缘改响应头这条路本来就是给这类“改不了源站”的场景准备的。缓存与回源规则那套面板里一般也有响应头规则这一项。

第三档,平台就是不给。内容平台的图床、标签管理器、同意管理弹窗,这一档普查里全是0%,而且短期内不会变。

第三档怎么办:换四个口径接着测

测不到细分时间,不等于什么都测不到。四个替代口径,按可信度排:

一、用总时长做粗筛。durationresponseEnd没被抹掉。它给不了归因,但能给排序——把首屏所有跨源资源按总时长排一遍,最长的那几个就是候选。粗,但能用。

二、用最大内容绘制条目自带的两个时刻。前面那张表里renderTimeloadTime都是可读的,两者之差就是“下载完到画上屏”这段。这段读得到,说明如果差值很小、总时刻却很晚,问题在下载之前,也就是在你看不见的那五段里。这个推论虽然绕,但它把范围收窄了一半。

三、做对照实验,不做归因。既然读不出握手花了多久,那就加一条预连接,然后看指标动没动。和前端一起做体验优化的时候这套办法本来就常用:读不到中间量,就用改前改后的端到端差值倒推。慢,但结论硬。

四、把关键资源搬回同源。最彻底的一条。首屏主图、首屏字体、阻塞渲染的样式表,这三样如果能走同源,可观测性直接从零变满。图片这一类还有个额外好处:文件名、替代文本与压缩格式那一套也一并回到自己手里。字体在首屏的成本那篇算过一笔账,字体这一类尤其值得搬——它既阻塞渲染,又几乎总是跨源。

先花两分钟,把自己站上的盲区列出来

不用装任何东西,打开首页,在浏览器控制台里跑这一段:

performance.getEntriesByType('resource')
  .filter(e => e.requestStart === 0 && e.responseEnd > 0)
  .map(e => ({host: new URL(e.name).host, type: e.initiatorType,
              ms: Math.round(e.duration)}))

判据是这两个条件同时成立:requestStart是0,说明细分时间被抹了;responseEnd大于0,说明这次请求真的发生过。两个条件合起来,基本就能把“许可缺失”和“缓存命中”分开。

跑出来的列表按主机名归组,通常只有两三个主机名,占了盲区的绝大部分。这就是你接下来要去谈的对象。

顺手也可以把有许可的那批捞出来对照一下,把e.requestStart - e.domainLookupStart排个序,看看握手这一段到底吃掉了多少——这个数在同源资源上一直都能读,只是很少有人专门去看。

一份能直接抄走的检查清单

  1. 打开首页,在控制台跑一遍资源计时,把requestStart为0的条目全列出来。这就是你的盲区清单。
  2. 数一下这份清单里有几个是首屏资源。首屏之外的暂时可以不管。
  3. 按主机名归组,看这些盲区落在几个托管方身上。通常是两到三个。
  4. 逐个查这几个托管方能不能加自定义响应头。能加的直接加通配符。
  5. 自有域名的资源,检查跳转链上每一跳都带了这行头,重点看那些只做转发的规则。
  6. 如果站上有服务端耗时明细,确认跨源那部分的计时许可也配了,否则那些数据从来没被读到过。
  7. 埋点代码里如果有判断renderTime是否为0的分支,标记为待清理。
  8. 统计字节数的埋点,加一列记录浏览器内核,否则那个300会把平均数拉平。

保哥自己带过的一个户外装备独立站,跑完第一步列出19个盲区条目,归组之后落在两个托管方上:一个是内容平台的图床,一个是同意管理弹窗。图床那边控制台里翻到了自定义响应头,加上之后首屏三张大图的时间线全开了,当场看出问题不在下载而在等待首字节。弹窗那家不给加,就按第三条做了对照实验,把它挪到交互后再加载,指标动了0.4秒。整件事花了一个下午,没写一行业务代码。

常见问题解答

这行头会不会带来安全或者隐私风险?

它暴露的是这次请求的时间点和字节数,不包含响应内容、不包含状态码、不包含任何头部值。真正需要谨慎的场景是:某个资源的存在与否、大小差异本身就是敏感信息,比如按登录状态返回不同大小的私有资源。对公开的静态资源——图片、脚本、样式表、字体——用通配符是行业常规做法,普查里501个资源就是这么配的。

配了它,跨域问题是不是也一起解决了?

不是。这两件事由两个不同的响应头管,实验台上单变量拆开测过:只配计时许可,五段时间全开、状态码仍然是0;只配跨域许可,状态码给了、五段时间仍然全零。字体加载、接口调用、画布取像素这些需求归跨域许可管,跟计时许可没有关系。反过来也一样,把跨域许可配得再全,时间该读不到还是读不到。

为什么控制台不报警告,只给一排零?

因为在规范眼里这不是错误,是正常的默认状态。跨源资源默认不暴露细分时间,这是设计如此;发那行头是站点主动放宽权限。既然默认状态不算异常,浏览器就没有理由提示。代价是使用方拿到零的时候,无法区分“没配许可”和“真的是零”——比如本地缓存命中时transferSize也是0。判据只能自己加:requestStart为0而responseEnd不为0,基本就是许可缺失。

这件事会直接影响排名吗?

不会。搜索引擎评估速度用的是真实用户的字段数据,那套采集走的是浏览器自己的通道,不依赖你站上有没有配这行头。它影响的是你的诊断能力——指标该多少还是多少,只是你查不出为什么。换句话说,它不改变体检结果,它只是把化验单上的分项全划掉了。

那些第三方挂件的时间读不到,还有别的办法定位吗?

有三条路,都是绕过去而不是打通。第一,用总时长排序做粗筛,读不到分项但读得到总量。第二,做加载策略的对照实验,把挂件改成交互后加载或者延后加载,看端到端指标动多少。第三,在真实用户监控里给这类资源单独打标,长期看它的总时长分布,异常的那几天再回头查。这三条都不如直接读分项,但都比“看到一排零然后放弃”强。

只有一半的跨源资源配了这个,是不是说明它没那么重要?

这个推论要小心。普查里还有一个数:1130个跨源资源里,单独配了计时许可而没配跨域许可的,是0个。也就是说这一项从来没有被谁主动开启过,它的覆盖率完全来自各家平台的默认模板。一个从没被主动配置过的项目,它的覆盖率反映的是默认值,不是需求强弱。真正的判据是问自己:上次查速度问题卡住的地方,是不是正好是一排零。

权威参考资料

分享到
标签
版权声明

本文标题:《做SEO盯了三年网站速度,页面上近一半的资源在监控里是一排零》

本文链接:https://zhangwenbao.com/cross-origin-timing-allow-origin-web-vitals-blind-spot.html

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

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