Performance API 实战指南 浏览器性能监控的那些坑我都踩过了

程序员篷璐 前端 阅读 2,605
赞 6 收藏
二维码
手机扫码查看
反馈

PerformanceAPI监控页面性能,折腾了一整天

上周遇到一个需求,要监控页面的加载性能数据,本来以为就是个简单的API调用,结果折腾了一整天才搞明白。这里把踩的坑都记录下来,免得下次再掉进去。

Performance 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支持程度不同,要做好兼容性处理

以上是我踩坑后的总结,希望对你有帮助。如果你有更好的方案欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论