登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

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 为资源加载提供高精度时间戳。与本文相关的字段如下。

阶段起点终点计算方式
DNSdomainLookupStartdomainLookupEnd终点 - 起点
完整连接connectStartconnectEnd终点 - 起点
TLSsecureConnectionStartconnectEnd仅安全连接且字段可见时计算
TLS 前连接connectStartsecureConnectionStart仅安全连接且字段可见时计算

有些资料也用 requestStart - secureConnectionStart 表示从 TLS 开始到请求发出的整体区间。本文为了严格拆分连接阶段,使用 connectEnd 作为 TLS 终点;如果业务关心“握手开始到真正发出请求”的等待,则可另外记录前一种区间,但不要把两种口径混在同一报表里。

PerformanceResourceTiming DNS 与 TLS 字段关系说明图

图1:DNS、连接与请求边界中的字段依赖;图中关系用于解释公式,不表示真实运行时间轴。

旧写法的问题:直接相减会制造假结论

function getNetworkCost(entry) {
  // 旧写法直接相减,没有区分不可观测、复用连接和非 HTTPS。
  return {
    dns: entry.domainLookupEnd - entry.domainLookupStart,
    tls: entry.connectEnd - entry.secureConnectionStart,
  };
}

这段代码至少有三个误区。

  1. 跨域字段受限:跨域响应没有 Timing-Allow-Origin 时,domainLookupStart、connectStart、secureConnectionStart、requestStart 等字段默认返回 0。此时相减结果不是有效网络测量。
  2. 非 HTTPS:普通 HTTP 没有 TLS 握手,secureConnectionStart 为 0,直接用 connectEnd - 0 会把相对时间戳误报为超大的 TLS 耗时。
  3. 缓存和连接复用:浏览器可能不需要重新解析 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,还要确认该响应头在缓存键、边缘节点和最终响应中按预期保留。

Resource Timing 可观测性与零值解释结构图

图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。这样监控平台看到的不是一堆含义混乱的零值,而是可以按来源、协议和连接状态解释的性能数据。

参考资料

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>