3、Navigation Timing:解析页面加载各阶段耗时

页面加载慢,到底慢在哪?

这是每个前端工程师都会遇到的问题。老板催你优化,你打开控制台一看,Network面板里一堆请求,红的绿的,但就是说不清具体哪里拖了后腿。

我个人习惯,第一件事就是打开 Navigation Timing API。它能把页面加载的整个过程拆成一个个阶段,像手术刀一样精准。DNS 查了多久?TCP 连了多久?TTFB 为什么那么高?一目了然。

3.1 Navigation Timing 是什么?

说白了,它就是浏览器提供的一组时间戳。从你输入 URL 按下回车,到页面完全渲染完毕,每个关键节点都被记录了下来。

这些数据藏在 window.performance.timing 对象里。你打开控制台,输入 performance.timing,就能看到一串数字。嗯,这些数字就是页面加载的“心电图”。

核心时间戳一览:

  • navigationStart:导航开始,用户按下回车的那一刻
  • fetchStart:浏览器开始获取资源
  • domainLookupStart / domainLookupEnd:DNS 查询开始和结束
  • connectStart / connectEnd:TCP 连接建立
  • requestStart:浏览器开始发送请求
  • responseStart:浏览器收到第一个字节(TTFB 的关键)
  • responseEnd:响应接收完毕
  • domLoading / domInteractive / domContentLoadedEventEnd:DOM 解析相关
  • loadEventEnd:页面完全加载完成

我记得刚入行时,以为页面加载就是“发请求-等响应-渲染”三步。后来看了这些时间戳才发现,光 DNS 查询就能吃掉几百毫秒,尤其是在移动端弱网环境下。

3.2 各阶段耗时计算

光看时间戳不够,你得算出差值。每个阶段的耗时,就是结束时间减去开始时间。

// DNS 查询耗时
const dnsTime = performance.timing.domainLookupEnd - performance.timing.domainLookupStart;

// TCP 连接耗时
const tcpTime = performance.timing.connectEnd - performance.timing.connectStart;

// TTFB(首字节时间)
const ttfb = performance.timing.responseStart - performance.timing.requestStart;

// 内容传输耗时
const contentDownloadTime = performance.timing.responseEnd - performance.timing.responseStart;

// DOM 解析耗时
const domParseTime = performance.timing.domInteractive - performance.timing.domLoading;

// 页面完全加载耗时
const loadTime = performance.timing.loadEventEnd - performance.timing.navigationStart;

你想想看,这些数据拿到手,你就能精准定位问题。TTFB 高,说明后端或者网络有问题。DNS 慢,说明域名解析服务需要优化。DOM 解析慢,说明 HTML 结构太复杂或者 JS 阻塞了渲染。

我的一个小习惯:把这些数据上报到监控平台,按百分位统计。P50 和 P95 的差距,往往能暴露很多问题。比如 P50 的 TTFB 是 200ms,P95 却到了 2s,那说明有部分用户遇到了网络波动或后端抖动。

3.3 TTFB:首字节时间,用户体验的第一道坎

TTFB 是我最关注的指标之一。它衡量的是从浏览器发出请求,到收到服务器返回的第一个字节所花的时间。

为什么重要?因为用户等的是“看到东西”,而不是“下载完”。TTFB 越长,用户的白屏时间就越长。

我曾经踩过一个坑:有一次线上页面 TTFB 突然飙升到 3s 以上。查了半天,发现是后端某个接口在高峰期做了全表扫描,数据库 CPU 打满。前端再怎么优化也没用,最后还是后端加了缓存才解决。

所以,TTFB 高的时候,别光盯着前端。先确认一下是不是后端接口慢了。

TTFB 的组成通常包括:

  • 网络延迟(RTT,往返时间)
  • 服务器处理时间(后端逻辑、数据库查询)
  • DNS 查询时间(如果没命中缓存)
  • TLS 握手时间(如果是 HTTPS)

我个人建议,TTFB 的目标值:

  • 优秀:小于 200ms
  • 一般:200ms - 500ms
  • 需要优化:大于 500ms

3.4 DNS 和 TCP:容易被忽略的“隐形杀手”

很多人觉得 DNS 和 TCP 是网络层的事,跟前端没关系。其实不然。

DNS 查询,说白了就是把你输入的域名翻译成 IP 地址。这个过程如果慢,用户连服务器都找不到。

TCP 连接,就是浏览器和服务器之间“握手”建立通道。三次握手,每次都要一个 RTT。

我记得有一次做海外站点优化,发现欧洲用户的 DNS 查询时间平均在 800ms 以上。后来换了支持 Anycast 的 DNS 服务商,直接降到了 100ms 以内。

优化建议:

  • DNS 预解析:在 HTML 头部添加 <link rel="dns-prefetch" href="//example.com">,提前解析域名
  • 预连接:使用 <link rel="preconnect" href="//example.com">,提前完成 TCP 和 TLS 握手
  • 减少域名数量:每个域名都要一次 DNS 查询,能合并就合并
  • 使用 CDN:CDN 节点离用户更近,能显著降低 RTT

3.5 实战:用代码采集并上报

光在控制台看没用,你得把这些数据采集下来,上报到监控平台。

function collectNavigationTiming() {
  // 等待页面完全加载
  window.addEventListener('load', () => {
    // 使用 requestAnimationFrame 确保所有资源加载完毕
    requestAnimationFrame(() => {
      setTimeout(() => {
        const timing = performance.timing;
        const navigation = performance.navigation;

        const metrics = {
          // 各阶段耗时
          dns: timing.domainLookupEnd - timing.domainLookupStart,
          tcp: timing.connectEnd - timing.connectStart,
          ttfb: timing.responseStart - timing.requestStart,
          download: timing.responseEnd - timing.responseStart,
          domParse: timing.domInteractive - timing.domLoading,
          domReady: timing.domContentLoadedEventEnd - timing.navigationStart,
          load: timing.loadEventEnd - timing.navigationStart,

          // 页面信息
          url: window.location.href,
          type: navigation.type, // 0: 导航, 1: 刷新, 2: 前进/后退
          timestamp: Date.now()
        };

        // 上报到监控平台
        reportMetrics(metrics);
      }, 0);
    });
  });
}

// 上报函数(示例)
function reportMetrics(data) {
  // 使用 sendBeacon 确保页面卸载时也能发送
  if (navigator.sendBeacon) {
    navigator.sendBeacon('/api/monitor', JSON.stringify(data));
  } else {
    // 降级方案:使用 Image 打点
    const img = new Image();
    img.src = `/api/monitor?data=${encodeURIComponent(JSON.stringify(data))}`;
  }
}

collectNavigationTiming();

注意:Navigation Timing API 在页面加载完成后才能获取完整数据。所以要在 load 事件后采集。另外,performance.timing 已经被标记为废弃,推荐使用 PerformanceObserver 或者 performance.getEntriesByType('navigation')。不过原理是一样的。

3.6 新版 API:PerformanceNavigationTiming

嗯,这里要注意一下。老版的 performance.timing 虽然还能用,但 W3C 已经推荐使用新的 PerformanceNavigationTiming 接口了。

// 获取导航性能条目
const [navigationEntry] = performance.getEntriesByType('navigation');

if (navigationEntry) {
  console.log('DNS 耗时:', navigationEntry.domainLookupEnd - navigationEntry.domainLookupStart);
  console.log('TCP 耗时:', navigationEntry.connectEnd - navigationEntry.connectStart);
  console.log('TTFB:', navigationEntry.responseStart - navigationEntry.requestStart);
  console.log('DOM 交互时间:', navigationEntry.domInteractive);
  console.log('页面完全加载时间:', navigationEntry.loadEventEnd);
}

新版的优势在于:

  • 返回的是 DOMHighResTimeStamp,精度更高(微秒级)
  • 支持 PerformanceObserver,可以异步监听
  • 数据更规范,更容易处理

3.7 总结一下

Navigation Timing 是性能监控的基石。它把页面加载拆成了 DNS、TCP、TTFB、DOM 解析等阶段,让你能精准定位瓶颈。

我个人建议,每个项目上线前,至少跑一遍这些指标。把数据上报到监控平台,持续跟踪。你会发现,很多性能问题其实早就埋下了,只是你没看到而已。

下一章,我们会深入 Resource Timing,看看如何监控单个资源的加载情况。到时候,连图片、脚本、样式表的加载细节都能抓出来。