前端监控自研调研文档——收集性能指标攻略

176 阅读7分钟

前言

随着Web应用复杂度的提升(如SPA、PWA、微前端架构的普及),用户对页面加载速度和交互流畅度的敏感度显著提高。数据显示:

  • 页面加载时间每增加1秒,移动端转化率下降约20%(Google研究)
  • 53%的用户会放弃加载时间超过3秒的页面(Akamai报告)

性能监控的目的是在前端应用中量化并收集性能指标,以便开发者更好,更有方向的去优化性能,此文以介绍笔者对性能监控的调研结果

关键性能指标定义

下面笔者先放出刻板的定义,在后面加入自己的理解

FP (First Paint)

定义:首次渲染时间,浏览器首次将任何像素绘制到屏幕的时刻

业务意义:反映浏览器开始渲染的最早信号,白屏时间的结束点

阈值标准:无明确标准,建议<=1s

FCP (First Contentful Paint)

定义:首次内容绘制,首次呈现文本/图片等非空白内容的时间

业务意义:用户留存关键指标,直接影响用户对页面的第一印象,影响跳出率

阈值标准:良好:<=1.8s;需改进:1.8-3s;差:>3s;

FID(First Input Delay,首次输入延迟)

定义:用户首次与页面交互(点击等)到浏览器实际响应操作的时间差

业务意义:衡量页面可交互性,影响用户体验流畅度

阈值标准:良好:<=100ms;需改进:100-300ms;差:>300ms;

CLS(Cumulative Layout Shift,累积布局偏移)

定义:页面生命周期内意外布局偏移的累计得分(如动态加载内容导致元素位移)

业务意义:防止用户误操作(如误点按钮),提升视觉稳定性

阈值标准:良好:≤0.1; 需改进:0.1-0.25; 差:> 0.25;

TTI(Time to Interactive,可交互时间)

定义:页面完全可交互的时间点(主线程空闲且能持续响应用户输入)

业务意义:反映用户何时能顺畅操作页面功能

阈值标准: 良好:≤3.8秒; 需改进:3.8~7.3秒; 差:>7.3秒;

TBT(Total Blocking Time,总阻塞时间)​

定义:FCP到TTI之间主线程被长任务(>50ms)阻塞的总时间

业务意义:量化页面响应延迟,影响用户感知的流畅度

阈值标准:良好:<= 300ms;需改进:300-600ms;差:>600ms

​LCP (Largest Contentful Paint)​

​定义​​:最大内容渲染时间,测量页面可视区域内最大文本/图片元素完成渲染的时间点

​业务意义​​:

  • 直接反映用户感知的主要内容加载速度,影响用户留存率(LCP每提升0.1秒,电商转化率可提升0.6%)

​阈值标准​​:

  • ​良好​​:≤2.5秒
  • ​需改进​​:2.5~4秒
  • ​差​​:>4秒

易混淆点

FPFCP

FP是指浏览器第一个像素点被渲染出来的时间点,白屏结束的象征,与客户主机的性能关联性较大,FCP则是第一个内容(文本,图片等)被渲染出来的时间点,除了客户端性能,与网络状态也有很大的关系

TTITBT

定义:TTI是页面可交互的时间点,反映什么时候用户能流程交互,而TBT是一个时间长度,他是长任务(>50ms)耗时的累计

技术实现方案

前置知识:浏览器原生API支持

PerformanceTiming

主要作用:获取从页面导航开始到加载完成的各个关键时间点的时间戳

关键时间戳及其含义​

属性名描述
navigationStart开始导航到页面的时间(用户输入URL或点击链接)
unloadEventStart前一个页面卸载开始的时间(若前页面与当前页面同域)
unloadEventEnd前一个页面卸载完成的时间
redirectStart第一个HTTP重定向开始的时间(若存在重定向)
redirectEnd最后一个HTTP重定向完成的时间
fetchStart浏览器开始获取页面资源(如HTML)的时间
domainLookupStartDNS查询开始的时间
domainLookupEndDNS查询完成的时间
connectStart开始与服务器建立TCP连接的时间
connectEndTCP连接建立完成的时间(包括SSL握手,如果使用HTTPS)
secureConnectionStartSSL握手开始的时间(仅HTTPS)
requestStart浏览器向服务器发送HTTP请求的时间
responseStart浏览器接收到服务器响应的第一个字节的时间(TTFB: Time to First Byte)
responseEnd浏览器接收到服务器响应的最后一个字节的时间
domLoading开始解析HTML文档的时间(DOM树构建开始)
domInteractiveDOM树构建完成,但外部资源(如图片)可能仍在加载
domContentLoadedEventStartDOMContentLoaded 事件触发前的时间(所有同步脚本执行完毕)
domContentLoadedEventEndDOMContentLoaded 事件完成的时间
domComplete页面所有资源(包括异步脚本)加载完成的时间
loadEventStartload 事件触发的时间
loadEventEndload 事件完成的时间

常见公式

const timing = window.performance.timing;

// DNS查询耗时
const dns = timing.domainLookupEnd - timing.domainLookupStart;

// TCP连接耗时
const tcp = timing.connectEnd - timing.connectStart;

// 首字节时间(TTFB)
const ttfb = timing.responseStart - timing.navigationStart;

// 内容加载完成时间(DOM Ready)
const domReady = timing.domContentLoadedEventEnd - timing.navigationStart;

// 页面完全加载时间
const loadTime = timing.loadEventEnd - timing.navigationStart;

Performance Timeline API

在 Performance Timing API 基础上,新增了更细粒度的性能监控能力:

​核心方法​

方法用途
performance.now()获取高精度时间戳(微秒级)
performance.mark(markName)手动标记一个时间点
performance.measure(name, startMark, endMark)计算两个标记点之间的耗时
performance.getEntries()获取所有性能条目(包括页面、资源、用户标记)
PerformanceObserver API​

相比传统的 performance.getEntries(),它支持动态监测新生成的性能数据,减少内存消耗,并提供更精细的控制。

基本用法​
  1. ​创建实例​
    通过构造函数传入回调函数,当监测到条目时执行:

    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries();
      entries.forEach(entry => {
        console.log('性能条目:', entry);
      });
    });
    
  2. ​订阅条目类型​
    调用 observe() 方法,指定监测的条目类型(entryTypes 或 type):

    // 旧方式:entryTypes 数组(监测多个类型)
    observer.observe({ entryTypes: ['resource', 'navigation'] });
    
    // 新方式:type 单个类型(推荐,需多次调用或创建多个实例)
    observer.observe({ type: 'resource', buffered: true });
    
  3. ​停止监测​
    使用 disconnect() 停止监听:

    observer.disconnect();
    
​使用场景示例​
  1. ​监测资源加载性能​

    const observer = new PerformanceObserver((list) => {
      list.getEntries().forEach(entry => {
        console.log(`${entry.name} 加载耗时: ${entry.duration}ms`);
      });
    });
    observer.observe({ type: 'resource' });
    
  2. ​捕获 Largest Contentful Paint (LCP)​

    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries();
      const lcpEntry = entries[entries.length - 1];
      console.log('LCP:', lcpEntry.startTime);
    });
    observer.observe({ type: 'largest-contentful-paint' });
    
  3. ​检测长任务​

    const observer = new PerformanceObserver((list) => {
      list.getEntries().forEach(entry => {
        console.log(`长任务耗时: ${entry.duration}ms`);
      });
    });
    observer.observe({ type: 'longtask' });
    
  4. ​CLS监控​

    let clsValue = 0;
    const observer = new PerformanceObserver(list => {
      list.getEntries().forEach(entry => {
        if (!entry.hadRecentInput) {  // 排除用户交互导致的布局变化
          clsValue += entry.value;
        }
      });
    });
    observer.observe({ type: 'layout-shift', buffered: true });
    

收集指标具体实现

基础指标收集(基于PerformanceTiming)

function getTimingMetrics() {
  const t = performance.timing;
  return {
    // DNS查询耗时
    dnsTime: t.domainLookupEnd - t.domainLookupStart,
    
    // TCP连接耗时
    tcpTime: t.connectEnd - t.connectStart,
    
    // 首字节时间(TTFB)
    ttfb: t.responseStart - t.navigationStart,
    
    // DOM解析时间
    domParseTime: t.domComplete - t.domInteractive,
    
    // 白屏时间(建议结合FP更准确)
    blankScreenTime: t.domLoading - t.navigationStart,
    
    // 页面完全加载时间
    fullLoadTime: t.loadEventEnd - t.navigationStart
  };
}

// 在load事件触发后获取
window.addEventListener('load', () => {
  const metrics = getTimingMetrics();
  console.log('基础指标:', metrics);
});

核心Web Vitals指标收集

1. FP/FCP收集
const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach(entry => {
    if (entry.name === 'first-paint') {
      console.log('FP:', entry.startTime);
    } else if (entry.name === 'first-contentful-paint') {
      console.log('FCP:', entry.startTime);
    }
  });
});

observer.observe({ type: 'paint', buffered: true });
2. LCP收集(最大内容渲染)
let lcpValue = 0;

const observer = new PerformanceObserver((list) => {
  const entries = list.getEntries();
  // 取最后一个条目(最大元素可能变化)
  const lastEntry = entries[entries.length - 1];
  lcpValue = lastEntry.startTime;
});

observer.observe({ type: 'largest-contentful-paint', buffered: true });

// 页面隐藏前上报数据(防止漏报)
document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    console.log('最终LCP:', lcpValue);
    // 上报逻辑
    observer.disconnect();
  }
});
3. CLS收集(布局偏移)
let clsValue = 0;

const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach(entry => {
    // 排除用户交互后的布局变化
    if (!entry.hadRecentInput) {
      clsValue += entry.value;
    }
  });
});

observer.observe({ type: 'layout-shift', buffered: true });

// 页面关闭前上报
window.addEventListener('beforeunload', () => {
  console.log('CLS:', clsValue);
});
4. FID收集(首次输入延迟)
const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach(entry => {
    // 记录首次输入延迟
    console.log('FID:', entry.processingStart - entry.startTime);
    // 只记录一次后断开
    observer.disconnect();
  });
});

observer.observe({ type: 'first-input', buffered: true });
5. 长任务监控(TBT相关)
let tbt = 0;
let fcpTime = 0;
let ttiTime = Infinity;

// 步骤1:获取FCP时间
new PerformanceObserver(list => {
  list.getEntries().forEach(entry => {
    if (entry.name === 'first-contentful-paint') {
      fcpTime = entry.startTime;
    }
  });
}).observe({ type: 'paint', buffered: true });

// 步骤2:获取TTI时间
getTTI().then((tti) => {
  ttiTime = tti;
});

// 步骤3:监控长任务并计算有效TBT
new PerformanceObserver((list) = > {
  list.getEntries().forEach(entry => {
    const taskStart = entry.startTime;
    const taskEnd = entry.startTime + entry.duration;
    
    // 仅处理 [FCP, TTI] 时间段内的长任务
    if (taskEnd < fcpTime || taskStart > ttiTime) return;

    const blockingTime = entry.duration - 50;
    if (blockingTime > 0) {
      tbt += blockingTime;
    }
  });
}).observe({ type: 'longtask', buffered: true });

总结

以上就是小编对于前段性能收集指标的调研,希望能对读者朋友们带来帮助!