前端监控(1)—— 性能监控(附实战代码)

2,882 阅读8分钟

一、性能监控?

性能监控是指页面或者说产品在用户使用中的用户实际使用效率数据监控,常见的性能监控数据有:

  • 首屏加载时间
  • 白屏时间
  • 请求的响应时间
  • 静态资源整体下载时间
  • 页面渲染时间
  • 首次可交互时间

上述性能监控的结果集,可以分析出前端性能的好坏,监测性能的结果可以指向性的去优化各块性能,比如常见的加快首屏加载等等。

二、性能监控的目的?

1.追求性能的极致是工程师的本分,关注前端性能是前端工程师的责任;

2.页面的性能直接关乎用户体验,开发设备的测试数据具有局限性,结合大量线上用户性能数据能更好的佐证页面性能,并对应优化。

3.资源(cdn)加载的异常能实时体现在上报数据中,能及时的处理资源加载问题。

三、用什么监控?

  • Performance

    Performance 接口可以获取到当前页面中与性能相关的信息。它是 High Resolution Time API 的一部分,同时也融合了 Performance Timeline API、Navigation Timing API、 User Timing API 和 Resource Timing API。

图片

浏览器控制台中输入 performance ,查看相关 API,此文章不做详细描述。

四: 性能监控关键数据介绍

  • Navigation Timing 获取的方式:
    • performance.timing(兼容性高,仍然可使用,未来可能被废弃)。
    // performance.timing;
    {
      connectEnd: 1600914654217,
      connectStart: 1600914654217,
      domComplete: 1600914654767,
      domContentLoadedEventEnd: 1600914654749,
      domContentLoadedEventStart: 1600914654744,
      domInteractive: 1600914654744,
      domLoading: 1600914654433,
      domainLookupEnd: 1600914654217,
      domainLookupStart: 1600914654217,
      fetchStart: 1600914654217,
      loadEventEnd: 1600914654767,
      loadEventStart: 1600914654767,
      navigationStart: 1600914654211,
      redirectEnd: 0,
      redirectStart: 0,
      requestStart: 1600914654230,
      responseEnd: 1600914654423,
      responseStart: 1600914654422,
      secureConnectionStart: 0,
      unloadEventEnd: 1600914654430,
      unloadEventStart: 1600914654430,	
    }
    
    • performance.getEntriesByType('navigation')[0] 使用了High-Resolution Time,时间精度可以达毫秒的小数点好几位,相比performance.timing精度更高(手机端兼容性不高,使用时应该向下兼容)
// performance.getEntriesByType('navigation')[0];
{
	connectEnd: 5.865000071935356,
    connectStart: 5.865000071935356,
    decodedBodySize: 1193,
    domComplete: 555.6950001046062,
    domContentLoadedEventEnd: 537.9900000989437,
    domContentLoadedEventStart: 533.555000089109,
    domInteractive: 533.4750000620261,
    domainLookupEnd: 5.865000071935356,
    domainLookupStart: 5.865000071935356,
    duration: 555.7300000218675,
    encodedBodySize: 383,
    entryType: "navigation",
    fetchStart: 5.865000071935356,
    initiatorType: "navigation",
    loadEventEnd: 555.7300000218675,
    loadEventStart: 555.7150000240654,
    name: "",
    nextHopProtocol: "h2",
    redirectCount: 0,
    redirectEnd: 0,
    redirectStart: 0,
    requestStart: 19.180000061169267,
    responseEnd: 211.83000004384667,
    responseStart: 210.77500004321337,
    secureConnectionStart: 5.865000071935356,
    startTime: 0,
    transferSize: 841,
    type: "navigate",
    unloadEventEnd: 219.0000000409782,
    unloadEventStart: 218.80500006955117,
    workerStart: 0,
}
  • 根据Navigation Timing计算主要性能数据:
上报字段描述计算公式备注
fptFirst Paint Time,首次渲染时间(白屏时间)。responseEnd - fetchStart从请求开始到浏览器开始解析第一批HTML文档字节的时间差。
ttiTime to Interact,首次可交互时间。domInteractive - fetchStart浏览器完成所有HTML解析并且完成DOM构建,此时浏览器开始加载资源。
readyHTML加载完成时间, 即DOM Ready时间。domContentLoadEventEnd - fetchStart如果页面有同步执行的JS,则同步JS执行时间 = ready - tti。
load页面完全加载时间loadEventStart - fetchStartload = 首次渲染时间 + DOM解析耗时 + 同步JS执行 + 资源加载耗时。
firstbyte首包时间responseStart - domainLookupStart
  • 区间段耗时字段含义
上报字段描述计算公式备注
dnsDNS查询耗时domainLookupEnd - domainLookupStart
tcpTCP连接耗时connectEnd - connectStart
ttfbTime to First Byte(TTFB),请求响应耗时。responseStart - requestStart
trans内容传输耗时responseEnd - responseStart
domDOM解析耗时domInteractive - responseEnd
res资源加载耗时loadEventStart - domContentLoadedEventEnd表示页面中的同步加载资源
sslSSL安全连接耗时connectEnd - secureConnectionStart只在HTTPS下有效
  • 页面资源加载时间获取方式
    • performance.getEntriesByType('resource'):返回页面所有资源的加载信息数组列表,单条数据结构为:
{
  connectEnd: 129.10999997984618
  connectStart: 129.10999997984618
  decodedBodySize: 83371
  domainLookupEnd: 129.10999997984618
  domainLookupStart: 129.10999997984618
  duration: 89.62500002235174
  encodedBodySize: 19423
  entryType: "resource"
  fetchStart: 129.10999997984618
  initiatorType: "link"
  name: "https://abc.css"
  nextHopProtocol: "h2"
  redirectEnd: 0
  redirectStart: 0
  requestStart: 192.6750000566244
  responseEnd: 218.73500000219792
  responseStart: 205.2350000012666
  secureConnectionStart: 129.10999997984618
  startTime: 129.10999997984618
  transferSize: 19823
  workerStart: 0
}

五、实战

  • 页面加载数据和页面资源数据前端获取
interface PageTimingInfo {
    // First Paint Time,首次渲染时间(白屏时间): 从请求开始到浏览器开始解析第一批HTML文档字节的时间差。。
    fpt: number;
    // Time to Interact,首次可交互时间: 浏览器完成所有HTML解析并且完成DOM构建,此时浏览器开始加载资源。。
    tti: number;
    // HTML加载完成时间, 即DOM Ready时间: 如果页面有同步执行的JS,则同步JS执行时间 = ready - tti。。
    ready: number;
    // 页面完全加载时间: load = 首次渲染时间 + DOM解析耗时 + 同步JS执行 + 资源加载耗时。
    loadTime: number;
    // 首包时间
    firstbyte: number;
    // DNS查询耗时
    dns: number;
    // TCP连接耗时
    tcp: number;
    // Time to First Byte(TTFB),请求响应耗时。
    ttfb: number;
    // 内容传输耗时
    trans: number;
    // DOM解析耗时
    dom: number;
    // 资源加载耗时(表示页面中的同步加载资源)
    res: number;
    // SSL安全连接耗时(只在HTTPS下有效)
    sslTime: number;
}

export default class PerformanceMonitor{

	static instance: PerformanceMonitor;
    
    static performance: Performance;
    
    // 资源请求超时边界值
    static TIMEOUT = 5000;
    
    // 单例模式
    static getInstance(): PerformanceMonitor {
        if (!PerformanceMonitor.instance) {
            PerformanceMonitor.instance = new PerformanceMonitor();
        }
        return PerformanceMonitor.instance;
    }
    
    constructor() {
        const performance = window.performance || window.msPerformance || window.webkitPerformance;
        if (!performance) {
            return
        }
        PerformanceMonitor.performance = performance;
        PerformanceMonitor.init();
    }
    
    // 获取页面加载数据
    static getPageTimeData(isTiming: boolean): PageTimingInfo {
        const {
            fetchStart,
            domainLookupStart,
            domainLookupEnd,
            connectStart,
            connectEnd,
            secureConnectionStart,
            requestStart,
            responseStart,
            responseEnd,
            domInteractive,
            domContentLoadedEventStart,
            domContentLoadedEventEnd,
            loadEventStart,
        } = isTiming ? PerformanceMonitor.performance.timing : PerformanceMonitor.performance.getEntriesByType('navigation');
        return {
            // First Paint Time,首次渲染时间(白屏时间): 从请求开始到浏览器开始解析第一批HTML文档字节的时间差。。
            fpt: responseEnd - fetchStart,
            // Time to Interact,首次可交互时间: 浏览器完成所有HTML解析并且完成DOM构建,此时浏览器开始加载资源。。
            tti: domInteractive - fetchStart,
            // HTML加载完成时间, 即DOM Ready时间: 如果页面有同步执行的JS,则同步JS执行时间 = ready - tti。。
            ready: domContentLoadedEventStart - fetchStart,
            // 页面完全加载时间: load = 首次渲染时间 + DOM解析耗时 + 同步JS执行 + 资源加载耗时。
            loadTime: loadEventStart - fetchStart,
            // 首包时间
            firstbyte: responseStart - domainLookupStart,
            // DNS查询耗时
            dns: domainLookupEnd - domainLookupStart,
            // TCP连接耗时
            tcp: connectEnd - connectStart,
            // Time to First Byte(TTFB),请求响应耗时。
            ttfb: responseStart - requestStart,
            // 内容传输耗时
            trans: responseEnd - responseStart,
            // DOM解析耗时
            dom: domInteractive - responseEnd,
            // 资源加载耗时(表示页面中的同步加载资源)
            res: loadEventStart - domContentLoadedEventEnd,
            // SSL安全连接耗时(只在HTTPS下有效)
            sslTime: secureConnectionStart ? (connectEnd - secureConnectionStart) : null
        }
    }
    
    // 获取页面资源加载数据
    static getTimeoutRes() {
        const resourceTimes = PerformanceMonitor.performance.getEntriesByType('resource');
        return resourceTimes ? resourceTimes.filter(({startTime, responseEnd}) => responseEnd - startTime > PerformanceMonitor.TIMEOUT)
        .map(({name, encodedBodySize, decodedBodySize, duration, nextHopProtocol, initiatorType}) => ({name, encodedBodySize, decodedBodySize, timeout: PerformanceMonitor.TIMEOUT, duration, protocol: nextHopProtocol, type: initiatorType})) : [];
    };
    
     static logPackage(): void {
        const {performance} = PerformanceMonitor;
        const resourceList = PerformanceMonitor.getTimeoutRes();
        const {userAgent} = navigator;
        const {location: {href: url}} = window;
        let pageTimeData;
        
        // 判断getEntriesByType是否可用,不可用使用timing(兼容性处理)
        if (performance.getEntriesByType('navigation').length > 0) {
            pageTimeData = PerformanceMonitor.getPageTimeData(false);
        } else if (performance.timing) {
            pageTimeData = PerformanceMonitor.getPageTimeData(true);
        }
        
        // 如果页面数据取到,防止performance.getEntriesByType('navigation')和performance.timing都不存在情况
        if (pageTimeData) {
            // 上传页面数据到后台
            createWebPerformance({...pageTimeData, userAgent, url});
        }
        
        // 如果超时资源文件存在
        if (resourceList.length) {
        	// 上传超时资源文件数据到后台
            createWebPerformanceResource({url,userAgent,resourceList})
        }
    }
    
    static bindEvent(): void {
    	// 用户是否有自定义window.onload方法
        const preLoad: (e: Event) => any = window.onload;
        window.onload = (e: Event) => {
            if (preLoad && typeof preLoad === 'function') {
                preLoad(e);
            }
            // 尽量不影响页面主线程
            if (window.requestIdleCallback) {
                window.requestIdleCallback(PerformanceMonitor.logPackage);
            } else {
                setTimeout(PerformanceMonitor.logPackage);
            }
        };
    }
    
    static init(): void {
        PerformanceMonitor.bindEvent();
    };
}
// 使用
PerformanceMonitor.getInstance();
  • 页面加载数据和页面资源数据后端数据库表结构(具体后端实现可以看我的github):
    • 页面加载主要性能数据表
    CREATE TABLE  IF NOT EXISTS `performance` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `equipmentId` int(11) NOT NULL COMMENT '设备id',
    `intervalId` int(11) NOT NULL COMMENT '区间id',
    `fpt` decimal(8,2) DEFAULT NULL COMMENT '白屏时间',
    `tti` decimal(8,2) DEFAULT NULL COMMENT '首次可交互时间',
    `ready` decimal(8,2) DEFAULT NULL COMMENT 'HTML加载完成时间',
    `loadTime` decimal(8,2) DEFAULT NULL COMMENT '页面完全加载时间',
    `firstbyte` decimal(8,2) DEFAULT NULL COMMENT '首包时间',
    `type` varchar(50) DEFAULT NULL COMMENT '页面打开方式',
    `createTime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    PRIMARY KEY (`id`)
    

) ENGINE=InnoDB DEFAULT CHARSET=utf8;

+ 页面加载区间数据表
```sql 
CREATE TABLE  IF NOT EXISTS `performance_interval` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`dns` decimal(8,2) DEFAULT NULL COMMENT 'DNS查询耗时',
`tcp` decimal(8,2) DEFAULT NULL COMMENT 'TCP连接耗时',
`ttfb` decimal(8,2) DEFAULT NULL COMMENT '请求响应耗时',
`trans` decimal(8,2) DEFAULT NULL COMMENT '内容传输耗时',
`dom` decimal(8,2) DEFAULT NULL COMMENT 'DOM解析耗时',
`res` decimal(8,2) DEFAULT NULL COMMENT '资源加载耗时',
`sslTime` decimal(8,2) DEFAULT NULL COMMENT 'SSL安全连接耗时',
`createTime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  • 页面资源加载超时表
CREATE TABLE  IF NOT EXISTS `performance_resource` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`equipmentId` int(11) NOT NULL COMMENT '设备id',
`name` longtext DEFAULT NULL COMMENT '文件名',
`encodedBodySize` int(11) DEFAULT NULL COMMENT '压缩之后body大小',
`decodedBodySize` int(11) DEFAULT NULL COMMENT '解压之后body大小',
`timeout` int(11) DEFAULT NULL COMMENT '超时时间边界值',
`duration` decimal(8,2) DEFAULT NULL COMMENT '请求总时间',
`protocol` varchar(50) DEFAULT NULL COMMENT '请求资源的网络协议',
`type` varchar(50) DEFAULT NULL COMMENT '发起资源的类型',
`createTime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  • 设备表
CREATE TABLE  IF NOT EXISTS `equipment` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`url` longtext DEFAULT NULL COMMENT '页面Url',
`userAgent` longtext DEFAULT NULL COMMENT '设备信息',
`ip` varchar(50) DEFAULT NULL COMMENT '用户ip',
`createTime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

最后

前端性能监控可以说是前端开发人员必备的知识,通过性能数据分析能更好的总结和优化网站,提升用户体验。

个人技术博客已经使用typescript + react hooks + antd 重构完成,有兴趣学习的小伙伴可以看看!

个人博客地址,有兴趣的可以看一看

github地址