Resource Timing API:精确测量 DNS、TCP、TTFB 与资源下载耗时

Resource Timing API:精确测量 DNS、TCP、TTFB 与资源下载耗时(讲座版)

各位开发者朋友,大家好!今天我们来深入探讨一个在前端性能优化中非常关键但又常被忽视的工具——Resource Timing API。如果你曾经试图准确分析页面加载过程中某个静态资源(比如图片、CSS、JS 文件)的各个阶段耗时,你会发现浏览器开发者工具虽然能提供一些信息,但不够细粒度、也不够稳定。而 Resource Timing API 正是为了解决这个问题应运而生的。


一、为什么我们需要更精确的性能数据?

在现代 Web 应用中,用户体验直接与加载速度挂钩。Google 的研究表明,用户对页面响应时间超过 3 秒的容忍度极低,会显著增加跳出率。因此,我们必须精准定位性能瓶颈。

常见的性能指标包括:

指标 含义 测量难点
DNS 查询时间 域名解析成 IP 所需时间 不同网络环境差异大,难以统计
TCP 连接时间 建立 TCP 握手所需时间 受服务器配置和网络延迟影响
TTFB(Time to First Byte) 从发起请求到收到第一个字节的时间 包含服务端处理逻辑,容易混淆
资源下载时间 实际传输资源内容的时间 易受带宽限制和 CDN 缓存策略影响

传统方式如 performance.timing 提供的是全局时间点,无法针对单个资源做细分。而 Resource Timing API 是专门为每个资源设计的细粒度监控方案,它允许我们捕获每一个 HTTP 请求生命周期中的详细时间戳。


二、什么是 Resource Timing API?

Resource Timing API 是 W3C 标准的一部分,属于 Performance API 的子集,通过 performance.getEntriesByType("resource") 获取所有加载资源的时间信息。

📌 它只适用于同源资源(Same-Origin Policy),跨域资源默认不暴露详细时间(除非服务器返回 Timing-Allow-Origin 头部)。

基本结构示例(简化版)

const entries = performance.getEntriesByType("resource");
entries.forEach(entry => {
    console.log({
        name: entry.name,           // 资源 URL
        duration: entry.duration,   // 总耗时(ms)
        initiatorType: entry.initiatorType, // 如 "link", "script", "img"
        transferSize: entry.transferSize, // 实际传输大小(bytes)
        decodedBodySize: entry.decodedBodySize, // 解码后大小(bytes)
        fetchStart: entry.fetchStart,
        domainLookupStart: entry.domainLookupStart,
        domainLookupEnd: entry.domainLookupEnd,
        connectStart: entry.connectStart,
        connectEnd: entry.connectEnd,
        requestStart: entry.requestStart,
        responseStart: entry.responseStart,
        responseEnd: entry.responseEnd
    });
});

这些字段构成了完整的资源加载链路图谱!


三、详解各阶段耗时计算公式

让我们逐个拆解关键阶段,并给出实用代码片段帮助你提取它们:

1. DNS 查询时间(DNS Lookup Time)

function getDnsTime(entry) {
    return entry.domainLookupEnd - entry.domainLookupStart;
}
  • domainLookupStart: DNS 开始查询时间
  • domainLookupEnd: DNS 查询结束时间

✅ 典型值:0~50ms(局域网快,公网慢)

2. TCP 连接时间(TCP Connection Time)

function getTcpTime(entry) {
    return entry.connectEnd - entry.connectStart;
}
  • connectStart: TCP 握手开始(SYN)
  • connectEnd: TCP 握手完成(ACK)

✅ 注意:如果使用了 HTTP/2 或 SPDY,这个值可能为 0(因为复用连接)

3. TTFB(Time to First Byte)

function getTtfb(entry) {
    return entry.responseStart - entry.requestStart;
}
  • requestStart: 发送请求开始时间
  • responseStart: 收到第一个字节时间

✅ 这个指标最能反映服务端处理能力,常见于 API 接口或 SSR 页面

4. 资源下载时间(Download Time)

function getDownloadTime(entry) {
    return entry.responseEnd - entry.responseStart;
}
  • responseStart: 第一个字节到达
  • responseEnd: 最后一个字节接收完毕

✅ 下载速度受限于带宽和文件大小,可结合 transferSizedecodedBodySize 分析压缩效率


四、完整实战案例:构建一个资源性能仪表盘

下面是一个完整的 HTML 示例,展示如何实时收集并可视化资源性能数据:

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8" />
    <title>Resource Timing Demo</title>
    <style>
        table { border-collapse: collapse; width: 100%; margin-top: 20px; }
        th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
        .highlight { background-color: #f0f9ff; font-weight: bold; }
    </style>
</head>
<body>

<h2>资源加载性能分析</h2>
<table id="performanceTable">
    <thead>
        <tr>
            <th>资源名称</th>
            <th>DNS 时间 (ms)</th>
            <th>TCP 时间 (ms)</th>
            <th>TTFB (ms)</th>
            <th>下载时间 (ms)</th>
            <th>总耗时 (ms)</th>
        </tr>
    </thead>
    <tbody></tbody>
</table>

<script>
// 监听 DOM 加载完成后执行
document.addEventListener('DOMContentLoaded', function () {
    const tableBody = document.querySelector('#performanceTable tbody');

    // 等待所有资源加载完成再收集数据
    window.addEventListener('load', function () {
        const resources = performance.getEntriesByType("resource");

        resources.forEach(entry => {
            if (!entry.transferSize || entry.initiatorType === 'navigation') return;

            const dnsTime = Math.round(getDnsTime(entry));
            const tcpTime = Math.round(getTcpTime(entry));
            const ttfb = Math.round(getTtfb(entry));
            const downloadTime = Math.round(getDownloadTime(entry));
            const totalDuration = Math.round(entry.duration);

            const row = document.createElement('tr');
            row.innerHTML = `
                <td>${entry.name}</td>
                <td class="${dnsTime > 50 ? 'highlight' : ''}">${dnsTime}</td>
                <td class="${tcpTime > 100 ? 'highlight' : ''}">${tcpTime}</td>
                <td class="${ttfb > 500 ? 'highlight' : ''}">${ttfb}</td>
                <td class="${downloadTime > 1000 ? 'highlight' : ''}">${downloadTime}</td>
                <td>${totalDuration}</td>
            `;
            tableBody.appendChild(row);
        });

        console.table(resources.map(r => ({
            name: r.name,
            dns: getDnsTime(r),
            tcp: getTcpTime(r),
            ttfb: getTtfb(r),
            download: getDownloadTime(r),
            total: r.duration
        })));
    });

    function getDnsTime(entry) {
        return entry.domainLookupEnd - entry.domainLookupStart;
    }

    function getTcpTime(entry) {
        return entry.connectEnd - entry.connectStart;
    }

    function getTtfb(entry) {
        return entry.responseStart - entry.requestStart;
    }

    function getDownloadTime(entry) {
        return entry.responseEnd - entry.responseStart;
    }
});
</script>

</body>
</html>

📌 说明

  • 使用 window.addEventListener('load') 确保所有资源加载完毕后再读取。
  • 对于大型项目,可以考虑使用 PerformanceObserver 来异步监听新增资源(见下文进阶部分)。
  • 表格中用 .highlight 类标记异常耗时项(如 DNS > 50ms、TTFB > 500ms),便于快速识别问题。

五、进阶技巧:PerformanceObserver + 自定义上报

对于生产环境,我们通常不会把原始数据打印在控制台,而是发送给日志服务(如 Sentry、LogRocket、自建埋点系统)。此时推荐使用 PerformanceObserver 实时监听资源变化。

const observer = new PerformanceObserver((list) => {
    list.getEntries().forEach(entry => {
        if (entry.entryType === "resource") {
            const data = {
                url: entry.name,
                dns: Math.round(entry.domainLookupEnd - entry.domainLookupStart),
                tcp: Math.round(entry.connectEnd - entry.connectStart),
                ttfb: Math.round(entry.responseStart - entry.requestStart),
                download: Math.round(entry.responseEnd - entry.responseStart),
                total: Math.round(entry.duration)
            };

            // 发送到后端或监控平台
            fetch('/api/performance-log', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(data)
            }).catch(err => console.error('Failed to log performance:', err));
        }
    });
});

observer.observe({ entryTypes: ['resource'] });

这样就能实现无侵入式的全量性能埋点,尤其适合 SPA(单页应用)动态加载资源的场景。


六、常见陷阱与注意事项

问题 原因 解决方案
数据为空或不完整 跨域资源未设置 Timing-Allow-Origin 服务端添加头:Timing-Allow-Origin: * 或具体域名
非同源资源无法获取 浏览器安全策略限制 使用 CORS 或代理转发请求
Chrome DevTools 中显示“Blocked by CORS” 请求被拦截导致未记录 检查网络面板是否有错误提示
多次加载同一资源(如缓存命中) getEntriesByType("resource") 只记录首次请求 使用 entry.initiatorType 过滤重复项
性能数据不准(特别是移动端) 网络波动、设备性能差异 结合多个维度(如 Lighthouse 报告)交叉验证

💡 小贴士:你可以用 performance.getEntriesByType("navigation") 获取整个页面导航的整体耗时,用于对比资源级数据是否合理。


七、总结:Resource Timing API 的价值

Resource Timing API 是前端性能监控体系的核心组件之一,它的优势在于:

  • ✅ 细粒度:每条资源都有独立的时间线
  • ✅ 可编程:可通过 JS 动态采集、过滤、聚合
  • ✅ 可扩展:结合 PerformanceObserver 实现自动化埋点
  • ✅ 可落地:支持多种上报机制(HTTP、WebSocket、Beacon)

无论你是要做性能监控平台、还是优化自家 CDN 策略、亦或是排查慢接口问题,掌握这个 API 都会让你事半功倍。


八、延伸阅读建议

如果你想进一步提升性能洞察力,推荐阅读以下资料:

主题 推荐链接
W3C Resource Timing Spec https://www.w3.org/TR/resource-timing-2/
MDN 文档 https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceResourceTiming
Google Developers – Performance Monitoring https://developers.google.com/web/fundamentals/performance/monitoring
Lighthouse 性能评分原理 https://github.com/GoogleChrome/lighthouse/blob/master/docs/performance-scoring.md

好了,今天的讲座就到这里。希望你能带着这份清晰的理解回到工作中,真正用好 Resource Timing API —— 让你的应用不仅更快,而且更可控、更可测!

如有疑问,欢迎留言交流。谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注