前端页面加载时间优化的实战经验分享
性能监控的核心数据怎么拿?
前端性能优化绕不开页面加载时间统计,这个数据直接影响用户体验。我之前做个项目,老板天天问页面到底多快,用户等待时间长不长,后来直接上了Performance API来精确测量。
核心代码其实很简单,就是在window.onload事件里获取各种时间节点:
window.addEventListener('load', function() {
const timing = performance.timing;
// 各种时间计算
const dnsTime = timing.domainLookupEnd - timing.domainLookupStart;
const tcpTime = timing.connectEnd - timing.connectStart;
const requestTime = timing.responseEnd - timing.requestStart;
const domReadyTime = timing.domContentLoadedEventEnd - timing.navigationStart;
const pageLoadTime = timing.loadEventEnd - timing.navigationStart;
console.log({
DNS查询时间: dnsTime,
TCP连接时间: tcpTime,
请求响应时间: requestTime,
DOM准备时间: domReadyTime,
页面完全加载: pageLoadTime
});
});
实际项目里的完整实现
上面那个是基础版本,真正上线肯定不能这么粗糙。我在jztheme.com的项目里用的是更完善的封装:
class PageLoadTracker {
constructor() {
this.init();
}
init() {
if (performance.timing) {
window.addEventListener('load', () => {
setTimeout(() => {
this.collectAndSend();
}, 0); // 确保DOM完全渲染完成
});
}
}
collectAndSend() {
const timing = performance.timing;
const data = {
navigationStart: timing.navigationStart,
unloadEventStart: timing.unloadEventStart,
unloadEventEnd: timing.unloadEventEnd,
redirectStart: timing.redirectStart,
redirectEnd: timing.redirectEnd,
fetchStart: timing.fetchStart,
domainLookupStart: timing.domainLookupStart,
domainLookupEnd: timing.domainLookupEnd,
connectStart: timing.connectStart,
connectEnd: timing.connectEnd,
secureConnectionStart: timing.secureConnectionStart,
requestStart: timing.requestStart,
responseStart: timing.responseStart,
responseEnd: timing.responseEnd,
domLoading: timing.domLoading,
domInteractive: timing.domInteractive,
domContentLoadedEventStart: timing.domContentLoadedEventStart,
domContentLoadedEventEnd: timing.domContentLoadedEventEnd,
domComplete: timing.domComplete,
loadEventStart: timing.loadEventStart,
loadEventEnd: timing.loadEventEnd
};
// 计算关键时间点
const calculated = {
DNS查询: data.domainLookupEnd - data.domainLookupStart,
TCP连接: data.connectEnd - data.connectStart,
SSL握手: data.secureConnectionStart ? data.connectEnd - data.secureConnectionStart : 0,
首字节时间: data.responseStart - data.requestStart,
请求响应: data.responseEnd - data.requestStart,
DOM解析: data.domComplete - data.domInteractive,
DOM准备: data.domContentLoadedEventEnd - data.navigationStart,
页面加载: data.loadEventEnd - data.navigationStart,
总耗时: data.loadEventEnd - data.navigationStart
};
// 发送到监控服务
this.sendToMonitor(calculated);
}
sendToMonitor(data) {
// 实际项目中替换为真实的上报接口
fetch('https://jztheme.com/api/performance', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: window.location.href,
timestamp: Date.now(),
...data
})
}).catch(err => {
console.error('性能数据上报失败:', err);
});
}
}
// 初始化
new PageLoadTracker();
这里注意我踩过好几次坑
第一,Timing API有个很大的限制:跨域资源的时间信息会被截断。如果请求不是同源的,responseStart、responseEnd这些字段都会变成0。这个问题我折腾了半天才发现。
第二,Timing API在某些老浏览器里支持不好,所以得加兼容判断:
if (performance.timing && performance.getEntriesByType) {
// 现代浏览器用 Navigation Timing Level 2
const navigation = performance.getEntriesByType('navigation')[0];
if (navigation) {
console.log(navigation);
}
} else if (performance.timing) {
// 老版本用 Timing API
console.log(performance.timing);
}
Navigation Timing Level 2确实更简洁,返回的是一个对象而不是一堆时间戳,但兼容性没Timing API好。建议两个都兼容一下。
更精确的资源加载监控
除了页面加载时间,资源加载情况也很重要。Resource Timing API可以监控所有资源的加载时间:
function getResourceTimings() {
const resources = performance.getEntriesByType('resource');
const resourceData = resources.map(resource => ({
name: resource.name,
entryType: resource.entryType,
startTime: resource.startTime,
duration: resource.duration,
transferSize: resource.transferSize,
encodedBodySize: resource.encodedBodySize,
decodedBodySize: resource.decodedBodySize,
connectStart: resource.connectStart,
connectEnd: resource.connectEnd,
responseStart: resource.responseStart,
responseEnd: resource.responseEnd
}));
return resourceData;
}
// 定期收集资源加载数据
setInterval(() => {
const resources = getResourceTimings();
// 只上报大文件或者加载时间长的资源
const slowResources = resources.filter(r => r.duration > 1000 || r.transferSize > 1024 * 100);
if (slowResources.length) {
console.log('慢资源:', slowResources);
}
}, 5000);
移动端的一些特殊考虑
移动端网络环境复杂,单纯记录加载时间意义不大。我在移动端增加了网络类型判断:
function getNetworkInfo() {
const connection = navigator.connection ||
navigator.mozConnection ||
navigator.webkitConnection;
if (connection) {
return {
effectiveType: connection.effectiveType, // 4g, 3g, 2g, slow-2g
downlink: connection.downlink, // Mbps
rtt: connection.rtt, // ms
saveData: connection.saveData // 是否开启了节省数据模式
};
}
return null;
}
// 结合网络信息上报
sendToMonitor(data) {
const networkInfo = getNetworkInfo();
fetch('https://jztheme.com/api/performance', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: window.location.href,
timestamp: Date.now(),
networkInfo,
...data
})
});
}
数据可视化怎么做
光收集数据没用,得能看出来趋势变化。我一般会在后台做个简单的图表展示,重点监控FP(First Paint)、FCP(First Contentful Paint)、LCP(Largest Contentful Paint)这些关键指标。
// 获取现代性能指标
function getModernMetrics() {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (entry.entryType === 'paint') {
console.log(${entry.name}: ${entry.startTime}ms);
} else if (entry.entryType === 'largest-contentful-paint') {
console.log(LCP: ${entry.startTime}ms);
}
});
});
observer.observe({ entryTypes: ['paint', 'largest-contentful-paint'] });
}
这套监控系统上线后,我们团队明显感觉到了页面性能的变化,特别是在新功能上线前后的对比上,数据说话比什么都有说服力。
以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多,比如结合错误监控、用户行为分析等等,后续会继续分享这类博客。

暂无评论