为什么统计资源加载耗时? (意义)
统计全站资源加载耗时是前端性能优化的一个重要环节,其意义主要体现在以下几个方面:
-
发现性能瓶颈: 通过监控每个资源的加载时间,可以轻易地找出加载缓慢的资源,例如过大的图片、加载缓慢的第三方脚本、响应延迟的API请求、未优化的字体文件等。这些慢资源往往是导致页面整体加载慢的主要原因。
-
优化用户体验 (UX): 页面加载速度直接影响用户体验。用户对慢速网站的容忍度很低,加载慢会导致用户流失、跳出率增加。通过优化资源加载,可以显著提升页面呈现速度,给用户带来流畅的体验。
-
指导性能优化方向: 数据是优化决策的基础。知道哪些资源加载慢后,我们可以有针对性地进行优化,比如:
- 图片优化(压缩、WebP格式、懒加载)
- 脚本优化(异步加载、延迟加载、代码分割、压缩、CDN)
- 样式优化(关键CSS、代码分割、压缩)
- 字体优化(Woff2格式、子集化、异步加载)
- 网络层面(启用Gzip/Brotli压缩、利用浏览器缓存、升级HTTP/2/3、使用CDN)
- 服务器层面(提高响应速度)
-
监控和预警: 将这些性能数据上报到后端进行收集和分析,可以建立性能监控系统。当某个资源的加载时间超过阈值时,可以触发预警,及时发现问题,避免影响大范围用户。
-
衡量优化效果: 在实施了性能优化措施后,可以通过持续的资源加载耗时监控来衡量优化效果,验证改进是否带来了预期的性能提升。
-
容量规划和成本控制: 通过分析资源加载的流量和时间,可以更好地理解网站的资源消耗,为服务器、CDN等的容量规划和成本控制提供数据支持。
-
A/B测试: 在测试不同的资源加载策略、新的图片格式或第三方库时,可以利用资源加载耗时数据来比较不同方案的性能差异。
总而言之,统计资源加载耗时能够帮助我们量化前端性能,找出问题所在,指导优化实践,持续监控网站的健康状况,最终提升用户体验和业务转化率。
如何统计资源加载耗时? (代码讲解)
现代浏览器提供了 Performance API,这是一个非常强大的工具,用于测量网页加载和运行时的性能。其中,PerformanceResourceTiming 接口提供了关于文档依赖资源加载的信息。
我们主要使用以下 API:
performance.getEntriesByType('resource'): 这个方法可以获取页面加载过程中所有类型为'resource'的性能条目列表。通常在页面加载完成后(例如window.onload事件中)调用,以获取一个完整的资源列表快照。PerformanceObserver: 这是更推荐的方式。它允许你异步地观察性能条目,并在条目生成时立即获取它们。这对于监控动态加载的资源(如懒加载图片、通过JS添加的CSS/JS)非常有用,且对页面主线程的阻塞更小。
方法一:使用 performance.getEntriesByType('resource') (快照方式)
这种方法简单直接,适合在页面加载完成后获取所有资源的加载信息。
优点: 简单易懂,容易实现。
缺点: 只能获取到在调用时已经加载完成或开始加载的资源,对于后续动态加载的资源无法捕获。等待 load 事件可能丢失一些早期加载的资源信息(尽管 buffered: true 在 PerformanceObserver 中可以缓解这个问题)。
// 等待整个页面及其所有资源都加载完成
window.addEventListener('load', () => {
// 获取所有类型为 'resource' 的性能条目
const resourceEntries = performance.getEntriesByType('resource');
console.log('--- 资源加载耗时统计 (快照方式) ---');
if (resourceEntries.length > 0) {
resourceEntries.forEach(entry => {
// entry 是 PerformanceResourceTiming 类型的对象
const resourceName = entry.name; // 资源URL
const startTime = entry.startTime; // 资源开始加载的时间戳
const endTime = entry.responseEnd; // 资源加载完成的时间戳 (接收到最后一个字节)
const duration = entry.duration; // 资源加载的总耗时 (endTime - startTime)
// 还可以获取更多信息,例如:
const initiatorType = entry.initiatorType; // 资源是如何被加载的 (如 img, script, link, fetch, xmlhttprequest 等)
const decodedBodySize = entry.decodedBodySize; // 解码后的资源大小 (字节)
const transferSize = entry.transferSize; // 实际传输的资源大小 (考虑压缩等,字节)
console.log(`
资源URL: ${resourceName}
发起方式: ${initiatorType}
开始时间: ${startTime.toFixed(2)} ms
结束时间: ${endTime.toFixed(2)} ms
总耗时: ${duration.toFixed(2)} ms
资源大小 (解码): ${decodedBodySize} bytes
资源大小 (传输): ${transferSize} bytes
--------------------
`);
// TODO: 将这些数据收集起来,准备上报
// collectAndReportData({
// url: resourceName,
// initiator: initiatorType,
// duration: duration,
// decodedSize: decodedBodySize,
// transferSize: transferSize
// });
});
} else {
console.log('没有找到资源性能条目。');
}
});
// 假设一个上报数据的函数
function collectAndReportData(data) {
// 在这里可以将数据暂存,然后批量或在合适时机发送到后端接口
console.log('准备上报数据:', data);
// fetch('/api/performance/report', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json'
// },
// body: JSON.stringify(data)
// });
}
解释:
window.addEventListener('load', ...): 确保在所有主要资源加载完成后执行代码。performance.getEntriesByType('resource'): 获取一个数组,数组中的每个元素都是一个PerformanceResourceTiming对象。PerformanceResourceTiming对象包含了资源的各种性能时间点和大小信息。name是资源的 URL,duration是从开始加载到加载完成的总时间。startTime,responseEnd,duration都是从页面导航开始(performance.timing.navigationStart)算起的时间偏移量,单位是毫秒。duration直接等于responseEnd - startTime。initiatorType很有用,可以知道这个资源是<img src="...">还是<script src="...">还是Workspace('/api/...')等等。decodedBodySize和transferSize可以帮助你了解资源的实际大小和网络传输的效率。
方法二:使用 PerformanceObserver (异步实时方式)
这是更灵活和推荐的方式,可以捕获页面生命周期中任何时候加载的资源。
优点: 能够捕获动态加载的资源,对页面性能影响较小,更适合长期监控。
缺点: 代码结构稍微复杂一些。
// 创建一个 PerformanceObserver 实例
const observer = new PerformanceObserver((list, obs) => {
// list 是一个 PerformanceObserverEntryList 对象,包含了观察到的性能条目
const entries = list.getEntries();
console.log('--- 资源加载耗时统计 (PerformanceObserver 方式) ---');
entries.forEach(entry => {
// PerformanceResourceTiming 类型的条目是我们关注的
if (entry.entryType === 'resource') {
const resourceName = entry.name; // 资源URL
const startTime = entry.startTime; // 资源开始加载的时间戳
const endTime = entry.responseEnd; // 资源加载完成的时间戳 (接收到最后一个字节)
const duration = entry.duration; // 资源加载的总耗时 (endTime - startTime)
const initiatorType = entry.initiatorType;
const decodedBodySize = entry.decodedBodySize;
const transferSize = entry.transferSize;
console.log(`
资源URL: ${resourceName}
发起方式: ${initiatorType}
开始时间: ${startTime.toFixed(2)} ms
结束时间: ${endTime.toFixed(2)} ms
总耗时: ${duration.toFixed(2)} ms
资源大小 (解码): ${decodedBodySize} bytes
资源大小 (传输): ${transferSize} bytes
--------------------
`);
// TODO: 将这些数据收集起来,准备上报
// collectAndReportData({
// url: resourceName,
// initiator: initiatorType,
// duration: duration,
// decodedSize: decodedBodySize,
// transferSize: transferSize
// });
}
});
// 如果你只需要捕获一次资源加载信息,可以在这里断开观察
// obs.disconnect();
});
// 开始观察类型为 'resource' 的性能条目
// { buffered: true } 表示观察者创建之前发生的条目也会被添加到第一次 list.getEntries() 中
observer.observe({ type: 'resource', buffered: true });
// 假设一个上报数据的函数 (同上)
function collectAndReportData(data) {
// 在这里可以将数据暂存,然后批量或在合适时机发送到后端接口
console.log('准备上报数据:', data);
// fetch('/api/performance/report', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json'
// },
// body: JSON.stringify(data)
// });
}
// 注意:对于 PerformanceObserver 方式,你需要在合适的时机上报数据。
// 可以设置一个定时器,或者在用户离开页面前使用 navigator.sendBeacon (推荐用于上报数据)
// 或者监听 visibilitychange 事件在页面不可见时上报。
// 示例:在页面隐藏时上报数据 (假设你将数据暂存在一个数组中)
// const performanceData = []; // 假设全局或闭包变量存储数据
// function collectAndReportData(data) {
// performanceData.push(data);
// }
// document.addEventListener('visibilitychange', () => {
// if (document.visibilityState === 'hidden') {
// // 使用 sendBeacon 不会阻塞页面卸载
// if (performanceData.length > 0) {
// const blob = new Blob([JSON.stringify(performanceData)], { type: 'application/json' });
// navigator.sendBeacon('/api/performance/report', blob);
// performanceData.length = 0; // 清空已发送的数据
// }
// }
// });
解释:
new PerformanceObserver(...): 创建一个观察者实例。- 回调函数
(list, obs) => { ... }: 当有新的符合条件的性能条目生成时,这个函数就会被调用。list包含了这次触发的所有新条目。 list.getEntries(): 获取这次触发的所有性能条目。entry.entryType === 'resource': 确保我们处理的是资源加载条目。observer.observe({ type: 'resource', buffered: true }): 配置观察者,使其关注类型为'resource'的条目。buffered: true非常重要,它可以让观察者获取到在它被创建之前就已经生成但仍在缓冲区中的条目,这能捕获到页面早期加载的资源。- 数据上报:
PerformanceObserver是异步的,它会在资源加载完成后触发。你需要一个机制来收集这些数据,并在合适的时机(如页面卸载前、页面隐藏时、或者收集到一定数量时)将它们发送到后端。navigator.sendBeacon是一个非常适合这种场景的API,它可以在页面卸载时发送少量数据,而不会延迟页面的关闭。
关于 PerformanceResourceTiming 的更多属性
除了上面提到的,PerformanceResourceTiming 对象还有一些其他有用的属性,可以提供更详细的网络时间信息(这些属性构成了著名的“瀑布图”):
domainLookupStart,domainLookupEnd: DNS查询开始和结束的时间。connectStart,connectEnd: TCP连接建立开始和结束的时间。secureConnectionStart: TLS握手开始的时间 (如果是HTTPS)。requestStart: 浏览器发送HTTP请求的时间。responseStart: 浏览器接收到第一个字节的时间。responseEnd: 浏览器接收到最后一个字节的时间。
通过组合这些时间点,你可以分析出更细粒度的性能问题,例如是DNS慢、TCP连接慢、TTFB (Time To First Byte) 长还是内容下载慢。
总耗时 duration 就是 responseEnd - startTime。
TTFB 约等于 responseStart - requestStart (更精确的TTFB会从connectEnd或secureConnectionEnd算起,取决于是否需要建立新连接或TLS)。
总结
统计前端资源加载耗时对于理解和优化网站性能至关重要。通过利用 Performance API,特别是 performance.getEntriesByType('resource') 和 PerformanceObserver,我们可以方便地获取每个资源的详细加载时间和其他性能信息。
推荐使用 PerformanceObserver 的方式进行监控,因为它更灵活、更高效,能够捕获动态加载的资源。结合 navigator.sendBeacon API,可以实现可靠的数据上报,为后续的性能分析、监控和优化提供坚实的数据基础。
在实际应用中,你还需要考虑如何收集、存储和分析这些大量的数据,可能需要搭建一个专门的性能监控平台或者集成到现有的APM (Application Performance Monitoring) 系统中。同时,注意数据的采样率,避免产生过大的流量。