Performance API 实战指南 浏览器性能监控的那些坑我都踩过了
PerformanceAPI监控页面性能,折腾了一整天
上周遇到一个需求,要监控页面的加载性能数据,本来以为就是个简单的API调用,结果折腾了一整天才搞明白。这里把踩的坑都记录下来,免得下次再掉进去。
最开始我想着直接用Performance API就行了,毕竟MDN文档看起来挺简单的。但是实际用起来才发现,这些API返回的数据结构比想象中复杂得多,而且不同浏览器的支持程度也有差异。
基本的Performance API获取方式
首先说最基础的获取性能数据的方法:
// 获取完整的导航时序数据
const perfData = performance.getEntriesByType('navigation')[0];
console.log(perfData);
// 获取资源加载数据
const resourceData = performance.getEntriesByType('resource');
console.log(resourceData);
// 获取页面加载时间
const timing = performance.timing;
console.log(timing);
上面这个代码看起来很直观,但这里我踩了个坑。在Chrome中,performance.getEntriesByType('navigation') 返回的数据是有的,但在某些老版本浏览器中可能返回空数组。后来查了一下发现,需要确保在DOM完全加载后才能获取到完整的数据。
准确获取页面加载各个阶段的时间
真正的难点在于如何准确计算各个阶段的加载时间。官方文档写得很模糊,我花了很长时间才整理出这套计算方法:
function getPerformanceMetrics() {
const perfData = performance.getEntriesByType('navigation')[0];
if (!perfData) {
console.warn('无法获取性能数据');
return null;
}
return {
// DNS查询时间
dnsTime: perfData.domainLookupEnd - perfData.domainLookupStart,
// TCP连接时间
tcpTime: perfData.connectEnd - perfData.connectStart,
// 请求响应时间 (TTFB)
ttfb: perfData.responseStart - perfData.requestStart,
// 首字节时间
firstByteTime: perfData.responseStart - perfData.navigationStart,
// DOM解析完成时间
domReadyTime: perfData.domContentLoadedEventEnd - perfData.navigationStart,
// 页面完全加载时间
loadCompleteTime: perfData.loadEventEnd - perfData.navigationStart,
// 白屏时间 (从开始到DOM解析完成)
whiteScreenTime: perfData.domContentLoadedEventStart - perfData.navigationStart
};
}
// 在页面完全加载后调用
window.addEventListener('load', () => {
setTimeout(() => {
const metrics = getPerformanceMetrics();
console.log('性能指标:', metrics);
}, 0);
});
这里有个需要注意的地方,就是setTimeout的作用。直接在load事件里获取可能会拿不到完整数据,延迟一下就能解决问题。这个坑我踩了好久才找到原因。
监控资源加载性能
除了页面本身的加载时间,还需要监控各种资源的加载情况。特别是图片、CSS、JS文件的加载时间:
function getResourceMetrics() {
const resources = performance.getEntriesByType('resource');
const resourceMetrics = resources.map(res => ({
name: res.name.split('/').pop(), // 文件名
type: getResourceType(res.name),
startTime: Math.round(res.startTime),
duration: Math.round(res.duration), // 加载耗时
size: res.transferSize || res.decodedBodySize || 0, // 传输大小
transferSize: res.transferSize || 0 // 实际传输大小
}));
return resourceMetrics;
}
function getResourceType(url) {
if (url.includes('.js')) return 'script';
if (url.includes('.css')) return 'stylesheet';
if (url.includes('.png') || url.includes('.jpg') || url.includes('.jpeg') || url.includes('.gif')) return 'image';
if (url.includes('.woff') || url.includes('.ttf')) return 'font';
return 'other';
}
// 监控资源加载
window.addEventListener('load', () => {
setTimeout(() => {
const resourceMetrics = getResourceMetrics();
console.log('资源加载指标:', resourceMetrics);
// 找出加载时间超过500ms的资源
const slowResources = resourceMetrics.filter(res => res.duration > 500);
if (slowResources.length > 0) {
console.warn('慢资源:', slowResources);
}
}, 1000); // 稍微延长时间,确保所有资源都加载完成
});
这里我发现一个问题,就是transferSize有时候会返回0,这时候需要用decodedBodySize来代替。不同的资源类型表现不一样,需要分别处理。
处理跨域资源的限制
最大的坑在这里,跨域资源的Performance API数据会被截断,大部分字段都是0:
function handleCrossOriginResource(resources) {
const filtered = [];
resources.forEach(res => {
// 对于跨域资源,只有部分字段可用
if (res.transferSize === 0 && res.decodedBodySize === 0) {
console.log(检测到跨域资源: ${res.name});
// 只能获取到加载时间,无法获取大小信息
filtered.push({
name: res.name,
duration: res.duration,
type: 'cross-origin',
sizeUnknown: true
});
} else {
filtered.push(res);
}
});
return filtered;
}
// 在实际项目中要这样处理
window.addEventListener('load', () => {
setTimeout(() => {
let resources = performance.getEntriesByType('resource');
resources = handleCrossOriginResource(resources);
console.log('处理后的资源数据:', resources);
}, 1000);
});
跨域资源的问题确实比较麻烦,如果需要精确的性能数据,最好把这些资源放在同源下,或者在服务器端设置相应的CORS头部。
上报性能数据到服务器
拿到数据后当然要上报给后端进行分析,这里有个小技巧,不要用XMLHttpRequest,会增加页面负载。用Beacon API更好:
function sendPerformanceData(data) {
// 检查是否支持Beacon API
if (navigator.sendBeacon) {
const blob = new Blob([JSON.stringify(data)], {
type: 'application/json'
});
// 发送到监控服务器
navigator.sendBeacon('https://jztheme.com/api/performance', blob);
} else {
// 降级处理
fetch('https://jztheme.com/api/performance', {
method: 'POST',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' }
}).catch(err => {
console.error('性能数据上报失败:', err);
});
}
}
// 完整的上报流程
function reportPerformance() {
setTimeout(() => {
const pageMetrics = getPerformanceMetrics();
const resourceMetrics = getResourceMetrics();
const performanceReport = {
timestamp: Date.now(),
url: window.location.href,
pageMetrics,
resourceMetrics,
userAgent: navigator.userAgent
};
sendPerformanceData(performanceReport);
}, 2000); // 确保所有资源都加载完成
}
// 页面卸载前上报
window.addEventListener('beforeunload', reportPerformance);
这里用了Beacon API,它有一个特性就是在页面关闭时也能发送数据,不会影响用户体验。不过要注意兼容性问题,老版本浏览器需要降级处理。
实际项目中的完整封装
最后把这个功能封装成一个类,在实际项目中使用:
class PerformanceMonitor {
constructor(options = {}) {
this.config = {
sampleRate: options.sampleRate || 1, // 采样率
threshold: options.threshold || 1000, // 慢资源阈值
onSlowResource: options.onSlowResource || null,
...options
};
this.init();
}
init() {
if (Math.random() > this.config.sampleRate) return;
window.addEventListener('load', () => {
setTimeout(() => {
this.collectAndReport();
}, 1500); // 给资源更多加载时间
});
}
collectPageMetrics() {
const nav = performance.getEntriesByType('navigation')[0];
if (!nav) return null;
return {
dnsTime: nav.domainLookupEnd - nav.domainLookupStart,
tcpTime: nav.connectEnd - nav.connectStart,
ttfb: nav.responseStart - nav.requestStart,
domReadyTime: nav.domContentLoadedEventEnd - nav.navigationStart,
loadCompleteTime: nav.loadEventEnd - nav.navigationStart,
whiteScreenTime: nav.domContentLoadedEventStart - nav.navigationStart
};
}
collectResourceMetrics() {
const resources = performance.getEntriesByType('resource');
return resources.map(res => ({
name: res.name.split('/').pop(),
type: this.getResourceType(res.name),
duration: Math.round(res.duration),
size: res.transferSize || res.decodedBodySize || 0,
transferSize: res.transferSize || 0
}));
}
getResourceType(url) {
if (url.includes('.js')) return 'script';
if (url.includes('.css')) return 'stylesheet';
if (/.(png|jpg|jpeg|gif|webp)$/.test(url)) return 'image';
if (/.(woff|woff2|ttf|eot)$/.test(url)) return 'font';
return 'other';
}
collectAndReport() {
try {
const data = {
timestamp: Date.now(),
url: window.location.href,
page: this.collectPageMetrics(),
resources: this.collectResourceMetrics()
};
this.analyzeData(data);
this.sendToServer(data);
} catch (error) {
console.error('性能监控收集失败:', error);
}
}
analyzeData(data) {
// 分析慢资源
const slowResources = data.resources.filter(res => res.duration > this.config.threshold);
if (slowResources.length > 0 && this.config.onSlowResource) {
this.config.onSlowResource(slowResources);
}
}
sendToServer(data) {
if (navigator.sendBeacon) {
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
navigator.sendBeacon('https://jztheme.com/api/performance', blob);
} else {
fetch('https://jztheme.com/api/performance', {
method: 'POST',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' }
}).catch(console.error);
}
}
}
// 在页面中使用
new PerformanceMonitor({
sampleRate: 0.1, // 10%采样
threshold: 800,
onSlowResource: (slowResources) => {
console.warn('发现慢资源:', slowResources);
}
});
折腾完这一套,感觉Performance API确实挺有用的,但坑也不少。特别是在不同浏览器下的表现差异,还有跨域资源的限制。目前这个版本基本能满足大部分性能监控需求了,虽然还有一些小问题(比如偶尔会有数据丢失的情况),但总体来说够用了。
踩坑提醒和注意事项
总结几个关键的踩坑点:
- 一定要在load事件后再获取性能数据,否则可能拿不到完整的数据
- 跨域资源只能获取到加载时间,无法获取大小信息
- setTimeout延迟很重要,给资源更多加载时间
- 上报数据用Beacon API,不会影响用户体验
- 不同浏览器的API支持程度不同,要做好兼容性处理
以上是我踩坑后的总结,希望对你有帮助。如果你有更好的方案欢迎评论区交流。

暂无评论