做SEO盯了三年网站速度,页面上近一半的资源在监控里是一排零
本文目录
- 网站速度做SEO,为什么报表上那个数是真的、定位却一步也走不下去?
- 那条分界线画在哪里
- 同源这条线,比大多数人以为的严
- 先看一眼这件事在真实站点上有多普遍
- 跨源资源上,到底哪几个字段被抹成了零?
- 同源和跨源,同一张图的两条记录
- 剩下的那几个字段,恰好是最没用的那几个
- 零这个值,最坏的地方在于它像个测量结果
- 已经配了跨域许可,为什么时间还是读不到?
- 单变量把两把钥匙拆开
- 现网360个资源只配了其中一把
- 顺带一个小发现:有9个资源把这行头发了两遍
- 同一张1.6 MB的图,为什么两个浏览器报出的字节数差了三个数量级?
- 300不是错误,是规范里的一个常量
- 这件事会怎么落到你的报表上
- 一条跳转链上少发一次这行头,会毁掉多少?
- 为什么这条特别容易踩
- 服务端埋好的耗时明细,为什么一条都没到前端?
- 最大内容绘制说得出是哪张图,为什么说不出它慢在哪一步?
- 两张表,一张什么都知道,一张什么都不知道
- 为什么这一层对做SEO的人特别难受
- 关于渲染时刻,有个说法已经过时了
- 顺手撞见的一条:两个引擎认的最大元素不是同一个
- 88个真实站点的首页,这一项到底配到了什么程度?
- 先看总量
- 再看按资源类型拆
- 最后看按托管方拆——这一栏解释了前面所有数字
- 这一项该怎么补?补不了的时候还能怎么测?
- 自有服务器上就是一行
- 写精确来源时,这四种写法全部静默失效
- 资源在别人手里的时候
- 第三档怎么办:换四个口径接着测
- 先花两分钟,把自己站上的盲区列出来
- 一份能直接抄走的检查清单
- 常见问题解答
- 这行头会不会带来安全或者隐私风险?
- 配了它,跨域问题是不是也一起解决了?
- 为什么控制台不报警告,只给一排零?
- 这件事会直接影响排名吗?
- 那些第三方挂件的时间读不到,还有别的办法定位吗?
- 只有一半的跨源资源配了这个,是不是说明它没那么重要?
- 权威参考资料
摘要:网站速度是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,同一台服务器,只换主机名:
| 字段 | 同源 | 跨源且没发那行头 |
|---|---|---|
startTime | 18.1 | 3.2 |
fetchStart | 18.1 | 3.2 |
domainLookupStart | 18.1 | 0 |
domainLookupEnd | 18.1 | 0 |
connectStart | 18.1 | 0 |
connectEnd | 18.1 | 0 |
secureConnectionStart | 18.1 | 0 |
requestStart | 18.6 | 0 |
responseStart | 18.9 | 0 |
responseEnd | 19.1 | 18.3 |
duration | 1 | 15.1 |
transferSize | 406 | 0 |
encodedBodySize | 106 | 0 |
decodedBodySize | 106 | 0 |
nextHopProtocol | http/1.1 | 空字符串 |
responseStatus | 200 | 0 |
两个引擎在这张表上的判决完全一致。
资源计时规范里对这件事的措辞是“不透明条目”。它没有含糊其辞,而是把被抹掉的字段一个个点了名:
原文是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 |
|---|---|---|---|
| 同源 | 1597409 | 1597409 | 1597109 |
| 跨源,只有计时许可 | 1597409 | 300 | 0 |
| 跨源,计时许可加跨域许可 | 1597409 | 1597409 | 1597109 |
| 跨源,什么都没有 | 0 | 0 | 0 |
第二行。同一张图,一个引擎说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 | 完整地址 |
| 最大内容绘制条目 | size | 558000 |
| 最大内容绘制条目 | renderTime | 68 |
| 最大内容绘制条目 | loadTime | 36.3 |
| 资源计时条目 | domainLookupStart | 0 |
| 资源计时条目 | connectStart | 0 |
| 资源计时条目 | requestStart | 0 |
| 资源计时条目 | responseStart | 0 |
| 资源计时条目 | transferSize | 0 |
同一张图,同一次加载,两张表。
上半张表把该说的都说了:是这个地址,占了这么大面积,几点画到屏幕上,几点下载完。
下半张表对同一张图片,五段时间加三个字节数,全是零。
指标告诉你“是这张图慢”,而唯一能告诉你“它慢在哪一步”的那张表,正好对这张图什么都不说。这不是监控没做好,这是两个接口各自的可见性规则拼出来的结果。
于是那个季度体检报告的追查就卡在这里:你知道罪魁祸首是哪张图了,接下来该判断是换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 | —— |
| 带计时许可的 | 561 | 49.6% |
| 有跨源资源的站 | 79 | —— |
| 至少有一个跨源资源读不到时间的站 | 68 | 86.1% |
| 跨源资源一个都没配的站 | 28 | 35.4% |
| 跨源资源全配齐的站 | 11 | 13.9% |
| 单站读不到的跨源资源中位数 | 3个 | —— |
差不多一半的跨源资源能读到时间。听起来还行,但站点层面的数字才是重点:86.1%的站至少有一个盲点,35.4%的站是全盲。
再看按资源类型拆
| 标签 | 探到数量 | 带计时许可 |
|---|---|---|
link(样式表与预载) | 386 | 71.2% |
img | 407 | 44.5% |
source | 46 | 39.1% |
script | 239 | 34.3% |
video | 15 | 33.3% |
iframe | 37 | 0% |
脚本34.3%,图片44.5%。这两类恰好是最需要被归因的——脚本决定主线程什么时候空出来,图片决定最大内容绘制什么时候发生。
嵌入框架37个,一个都没有。评价挂件、客服窗口、地图、视频播放器、支付按钮,基本都是这个形态。它们在你的性能报表里,从来就没有过时间。
最后看按托管方拆——这一栏解释了前面所有数字
| 资源主机 | 探到数量 | 带计时许可 |
|---|---|---|
cdn.shopify.com | 153 | 100% |
github.githubassets.com | 26 | 100% |
b.stripecdn.com | 22 | 100% |
res.cloudinary.com | 20 | 100% |
s.alicdn.com | 16 | 100% |
images.ctfassets.net | 49 | 0% |
cdn.prod.website-files.com | 41 | 0% |
www.googletagmanager.com | 33 | 0% |
cdn.sanity.io | 25 | 0% |
a.storyblok.com | 24 | 0% |
cdn.cookielaw.org | 14 | 0% |
definitions.sqspcdn.com | 13 | 0% |
没有中间地带。要么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.com和https://a.com/都是一回事,运维配的时候顺手带个斜杠是很自然的动作。这里不是。
一个只做逐字比对、失败后静默降级的配置项,它的真实错误率跟它的语法宽容度成反比。越是没人校验的字段,越应该用通配符,而不是精确值。
资源在别人手里的时候
这才是大多数人的处境。按普查那张按托管方拆的表,情况分三档。
第一档,平台默认就配了。建站平台自家CDN、几家主流图片处理服务、大型支付服务商的静态资源,实测都是100%。这一档你什么都不用做。
第二档,平台支持自定义响应头,藏在控制台某个不显眼的地方。可编程边缘那一层通常都能加,在边缘改响应头这条路本来就是给这类“改不了源站”的场景准备的。缓存与回源规则那套面板里一般也有响应头规则这一项。
第三档,平台就是不给。内容平台的图床、标签管理器、同意管理弹窗,这一档普查里全是0%,而且短期内不会变。
第三档怎么办:换四个口径接着测
测不到细分时间,不等于什么都测不到。四个替代口径,按可信度排:
一、用总时长做粗筛。duration和responseEnd没被抹掉。它给不了归因,但能给排序——把首屏所有跨源资源按总时长排一遍,最长的那几个就是候选。粗,但能用。
二、用最大内容绘制条目自带的两个时刻。前面那张表里renderTime和loadTime都是可读的,两者之差就是“下载完到画上屏”这段。这段读得到,说明如果差值很小、总时刻却很晚,问题在下载之前,也就是在你看不见的那五段里。这个推论虽然绕,但它把范围收窄了一半。
三、做对照实验,不做归因。既然读不出握手花了多久,那就加一条预连接,然后看指标动没动。和前端一起做体验优化的时候这套办法本来就常用:读不到中间量,就用改前改后的端到端差值倒推。慢,但结论硬。
四、把关键资源搬回同源。最彻底的一条。首屏主图、首屏字体、阻塞渲染的样式表,这三样如果能走同源,可观测性直接从零变满。图片这一类还有个额外好处:文件名、替代文本与压缩格式那一套也一并回到自己手里。字体在首屏的成本那篇算过一笔账,字体这一类尤其值得搬——它既阻塞渲染,又几乎总是跨源。
先花两分钟,把自己站上的盲区列出来
不用装任何东西,打开首页,在浏览器控制台里跑这一段:
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排个序,看看握手这一段到底吃掉了多少——这个数在同源资源上一直都能读,只是很少有人专门去看。
一份能直接抄走的检查清单
- 打开首页,在控制台跑一遍资源计时,把
requestStart为0的条目全列出来。这就是你的盲区清单。 - 数一下这份清单里有几个是首屏资源。首屏之外的暂时可以不管。
- 按主机名归组,看这些盲区落在几个托管方身上。通常是两到三个。
- 逐个查这几个托管方能不能加自定义响应头。能加的直接加通配符。
- 自有域名的资源,检查跳转链上每一跳都带了这行头,重点看那些只做转发的规则。
- 如果站上有服务端耗时明细,确认跨源那部分的计时许可也配了,否则那些数据从来没被读到过。
- 埋点代码里如果有判断
renderTime是否为0的分支,标记为待清理。 - 统计字节数的埋点,加一列记录浏览器内核,否则那个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