PerformanceResourceTiming 拆分 DNS 与 TLS 耗时
来源:17golang原创
时间:2026-10-01 19:34:29 108浏览 收藏
PerformanceResourceTiming 可以把资源请求的连接阶段拆成 DNS、TCP 和 TLS 三部分。DNS 使用 domainLookupEnd - domainLookupStart;完整连接使用 connectEnd - connectStart;对 HTTPS 连接,TLS 可按 connectEnd - secureConnectionStart 计算,TLS 之前的连接部分则是 secureConnectionStart - connectStart。
真正难点不在减法,而在 0 值解释:跨域资源没有开放 Resource Timing 时,相关字段会被隐藏为 0;DNS 缓存、HTTP 连接复用和资源缓存也可能让阶段耗时为 0。因此采集代码必须同时输出“数值”和“是否可解释”,不能把所有 0 都当成网络极快。
背景:一个连接耗时数字不够定位问题
假设监控平台只上报 connectEnd - connectStart,某个静态资源的连接阶段突然从几十毫秒升到数百毫秒。这个数字不能直接说明是 DNS 解析变慢、TCP 建连受阻,还是 TLS 握手成本上升。
PerformanceResourceTiming 为资源加载提供高精度时间戳。与本文相关的字段如下。
| 阶段 | 起点 | 终点 | 计算方式 |
|---|---|---|---|
| DNS | domainLookupStart | domainLookupEnd | 终点 - 起点 |
| 完整连接 | connectStart | connectEnd | 终点 - 起点 |
| TLS | secureConnectionStart | connectEnd | 仅安全连接且字段可见时计算 |
| TLS 前连接 | connectStart | secureConnectionStart | 仅安全连接且字段可见时计算 |
有些资料也用 requestStart - secureConnectionStart 表示从 TLS 开始到请求发出的整体区间。本文为了严格拆分连接阶段,使用 connectEnd 作为 TLS 终点;如果业务关心“握手开始到真正发出请求”的等待,则可另外记录前一种区间,但不要把两种口径混在同一报表里。

图1:DNS、连接与请求边界中的字段依赖;图中关系用于解释公式,不表示真实运行时间轴。
旧写法的问题:直接相减会制造假结论
function getNetworkCost(entry) {
// 旧写法直接相减,没有区分不可观测、复用连接和非 HTTPS。
return {
dns: entry.domainLookupEnd - entry.domainLookupStart,
tls: entry.connectEnd - entry.secureConnectionStart,
};
}
这段代码至少有三个误区。
- 跨域字段受限:跨域响应没有
Timing-Allow-Origin时,domainLookupStart、connectStart、secureConnectionStart、requestStart等字段默认返回 0。此时相减结果不是有效网络测量。 - 非 HTTPS:普通 HTTP 没有 TLS 握手,
secureConnectionStart为 0,直接用connectEnd - 0会把相对时间戳误报为超大的 TLS 耗时。 - 缓存和连接复用:浏览器可能不需要重新解析 DNS 或建立连接,阶段耗时为 0 并不代表采集失败;仅凭一个条目也不能断言具体命中了哪一层复用。
新规则:数值和状态一起返回
更稳妥的 API 不只返回毫秒数,还标记详细时间是否可见、是否存在新的安全握手,以及 0 值应如何解释。下面的函数保持口径单一,并对浮点误差做非负保护。
function splitConnectionTiming(entry) {
// requestStart 为 0 时,常见原因是跨域详细时间未通过 TAO 开放。
const detailedTimingVisible = entry.requestStart > 0;
if (!detailedTimingVisible) {
return {
observable: false,
reason: "timing-restricted-or-unavailable",
dnsMs: null,
tcpBeforeTlsMs: null,
tlsMs: null,
connectionMs: null,
};
}
const duration = (end, start) => Math.max(0, end - start);
const hasTlsHandshake = entry.secureConnectionStart > 0;
return {
observable: true,
reason: hasTlsHandshake ? "new-secure-connection" : "no-new-tls-handshake",
dnsMs: duration(entry.domainLookupEnd, entry.domainLookupStart),
connectionMs: duration(entry.connectEnd, entry.connectStart),
tcpBeforeTlsMs: hasTlsHandshake
? duration(entry.secureConnectionStart, entry.connectStart)
: duration(entry.connectEnd, entry.connectStart),
tlsMs: hasTlsHandshake
? duration(entry.connectEnd, entry.secureConnectionStart)
: null,
};
}
tlsMs: null 与 tlsMs: 0 应保持不同语义:null 表示当前条目没有可识别的新 TLS 握手;0 则表示握手字段存在,但在浏览器计时精度下差值为 0。监控后端也应保留这种区分,不要在序列化时把 null 强制转换成 0。
代码对比:用 PerformanceObserver 持续收集
页面初始化较晚时,单次 performance.getEntriesByType("resource") 只能看到调用当时仍在缓冲区中的条目。使用 PerformanceObserver 并设置 buffered: true,可以先接收观察器创建前已经记录的条目,再持续接收新增条目。
function reportResourceTiming(entry) {
const timing = splitConnectionTiming(entry);
// 只上报必要维度,避免把完整 URL 查询参数带入监控系统。
const url = new URL(entry.name);
const payload = {
origin: url.origin,
path: url.pathname,
initiatorType: entry.initiatorType,
nextHopProtocol: entry.nextHopProtocol,
transferSize: entry.transferSize,
...timing,
};
// 示例只输出结构;生产环境应批量发送并限制采样率。
console.log(payload);
}
// 长页面可增大缓冲区,默认 resource timing 缓冲条目数量有限。
performance.setResourceTimingBufferSize(1000);
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// observe 的 type 已限定 resource,这里按 Resource Timing 条目处理。
reportResourceTiming(entry);
}
});
observer.observe({ type: "resource", buffered: true });
采集 URL 时最好只保留 origin 和 pathname,移除查询参数与片段,避免把用户标识、签名或临时令牌送入性能平台。对于高流量页面,还应限制采样率和单页上报条数。
兼容注意:跨域 TAO 决定能否看到详细字段
同源资源通常可以读取完整 timing。跨域资源若希望被页面读取 DNS、连接、请求开始等详细时间,资源响应必须带 Timing-Allow-Origin。例如,只允许指定站点读取时可以返回:
# 由跨域资源服务器返回,仅允许指定来源读取详细 Resource Timing。 Timing-Allow-Origin: https://app.example.com
这是响应头,不是前端请求头。前端 JavaScript 自己添加同名请求头不会开放数据。若资源来自 CDN,还要确认该响应头在缓存键、边缘节点和最终响应中按预期保留。

图2:资源来源、TAO 响应头、缓存与连接复用共同影响最终可解释的 DNS/TLS 指标。
采用建议:先按可观测性分组,再比较耗时
采集到数据后,不要直接把所有资源混在一个平均值里。建议先按以下维度分组。
| 状态 | 字段表现 | 处理方式 |
|---|---|---|
| 详细时间受限 | requestStart 等受限字段为 0 | 记为不可观测,不纳入 DNS/TLS 平均值 |
| 新 HTTPS 连接 | secureConnectionStart > 0 | 分别记录 DNS、TLS 前连接与 TLS |
| 没有新 TLS 握手 | secureConnectionStart = 0,详细字段可见 | TLS 记为 null,并结合缓存/复用信息解释 |
| DNS 差值为 0 | 起止时间相等 | 可能是缓存或复用,不直接宣称 DNS 为零成本 |
transferSize === 0 常用于辅助判断资源是否来自本地缓存,nextHopProtocol 可以说明协商到的协议。但单个字段通常不足以还原浏览器内部连接池决策,性能报表应使用“可能复用”“未观察到新握手”等谨慎表述。
常见问题
为什么同一个域名只有第一个资源有 DNS 和 TLS 耗时?
后续资源可能复用了 DNS 结果和已建立连接,因此没有新的查询或握手阶段。这通常是浏览器连接管理的正常现象,应按资源批次或 origin 聚合,而不是要求每个资源都出现非零值。
TLS 应该用 connectEnd 还是 requestStart 作为终点?
若目标是拆分连接阶段,使用 connectEnd - secureConnectionStart 更贴合“安全连接建立完成”的边界;若要衡量从握手开始到请求即将发送的整体区间,可记录 requestStart - secureConnectionStart。两者命名和口径必须分开。
跨域字段全部为 0,前端能绕过吗?
不能靠前端代码绕过。需要跨域资源的响应端正确返回 Timing-Allow-Origin,并确保 CDN 或代理没有移除该头。
为什么观察器没有收到更早的资源?
需要使用 { type: "resource", buffered: true },同时注意 resource timing 缓冲区容量。长生命周期页面可提前创建观察器、增大缓冲区,并及时消费条目。
总结
用 PerformanceResourceTiming 拆分 DNS 与 TLS 的核心,是先定义稳定公式,再定义 0 值语义。DNS 用域名查询起止字段,TLS 用安全连接开始到连接结束字段;跨域受限返回不可观测,未发生新握手返回 null。这样监控平台看到的不是一堆含义混乱的零值,而是可以按来源、协议和连接状态解释的性能数据。
参考资料
-
194 收藏
-
427 收藏
-
480 收藏
-
486 收藏
-
165 收藏
-
381 收藏
-
291 收藏
-
294 收藏
-
142 收藏
-
288 收藏
-
392 收藏
-
110 收藏
-
251 收藏
-
380 收藏
-
269 收藏
-
249 收藏
-
167 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习