MonitorSDK_性能监控(从Web Vital性能指标、PerformanceObserver API和具体代码实现)

性能监控

性能指标

在实现性能监控前,先了解Web Vitals 涉及的常见的性能指标

Web Vitals 是由 Google 推出的网页用户体验衡量指标体系,旨在帮助开发者量化和优化网页在实际用户终端上的性能体验。Web Vitals 强调"以用户为中心"的度量,而不是纯技术层面的加载时间。

要按 先后顺序(时间维度) 梳理 Web Vitals,可以从网页加载的生命周期出发,把每个指标放入其发生时机对应的阶段中。这样更利于理解用户体验的演变和指标采集的逻辑。


🧭 一、加载过程的五大阶段

复制代码
[1] 网络响应阶段
[2] 首次渲染阶段
[3] 内容加载阶段
[4] 用户交互阶段
[5] 页面稳定阶段

📊 二、Web Vitals 指标按时间顺序梳理

阶段 指标名 含义 时机
1️⃣ 网络响应 TTFB (Time to First Byte) 首字节到达 浏览器请求后,接收到第一个响应字节
2️⃣ 首次渲染 FCP (First Contentful Paint) 首次绘制文字/图像 页面开始有内容渲染(非白屏)
3️⃣ 主内容加载 LCP (Largest Contentful Paint) 最大可视内容渲染完成 用户感知"页面加载完"
4️⃣ 用户首次交互 FID (First Input Delay) 用户首次点击的响应延迟 用户第一次交互,直到浏览器处理事件的延迟
5️⃣ 页面稳定 CLS (Cumulative Layout Shift) 布局跳动 页面是否因为图片/广告等加载而抖动

🧬 三、时间线图(逻辑顺序)

00.000 s 00.000 s 00.000 s 00.000 s 00.000 s 00.000 s 00.000 s 00.000 s 00.000 s 00.000 s 00.000 s 00.000 s 00.000 s 00.000 s 00.000 s 00.000 s 00.000 s DNS & TCP/SSL TTFB(首字节返回) FCP(首内容绘制) CLS(累积布局偏移) Paint & 字体加载 LCP(最大内容绘制) 用户输入(点击等) FID(首次输入延迟) 网络层 页面初渲 主体内容加载 用户交互 页面稳定性

时间单位为毫秒。TTFB 最早,CLS 贯穿整个加载过程。


🔁 四、简洁记忆顺序口诀

💡「先 TTFB,见 FCP;看大图,用 LCP;首操作,测 FID;别乱跳,查 CLS


🛠 五、指标采集时机小贴士

指标 采集方式 推荐 API
TTFB performance.timing.responseStart - navigationStart Navigation Timing
FCP PerformanceObserver 监听 paint PerformancePaintTiming
LCP PerformanceObserver 监听 largest-contentful-paint LCP Entry
FID 真实用户交互产生的事件延迟 Event Timing API
CLS PerformanceObserver 监听 layout-shift LayoutShift Entry

🧭 六、总结为时序流图(Mermaid)

User Browser Server 输入 URL 发起请求 返回首字节 (TTFB) 渲染第一屏内容 (FCP) 渲染最大元素 (LCP) 第一次点击或输入 记录 FID(输入延迟) 页面渲染抖动时记录 CLS User Browser Server
TTFB ≤ 800ms FCP ≤ 1.8s LCP ≤ 2.5s FID ≤ 100ms CLS ≤ 0.1 TBT ≤ 200ms


性能指标监控------PerformanceObserver

PerformanceObserver 是 Performance API 中用于监听性能条目变化 的核心工具。它可以在网页运行过程中,异步捕获新生成的性能条目 ,而不是一开始就调用 performance.getEntries() 拿"旧数据"。


一、PerformanceObserver 的作用

它允许开发者:

  • 监听网页运行中出现的性能条目(如资源加载、绘制、打点、长任务等)
  • 做出动态响应(如打日志发送埋点数据
  • 支持指定监听的 entryTypes,如 ["resource"], ["mark", "measure"]

二、使用方式

1. 创建实例
js 复制代码
const observer = new PerformanceObserver((list, observer) => {
  const entries = list.getEntries();
  for (const entry of entries) {
    console.log(entry.name, entry.entryType, entry.startTime, entry.duration);
  }
});
2. 启动监听
js 复制代码
observer.observe({
  entryTypes: ['mark', 'measure', 'resource']
});

entryTypes 是监听的条目类型数组。


三、常用方法

方法 说明
observe(options) 开始监听性能条目
disconnect() 停止监听
takeRecords() 获取当前缓冲区的所有性能条目并清空缓冲区

四、选项说明

observe(options)
js 复制代码
observer.observe({
  entryTypes: ['resource', 'paint']
});

或使用过时写法(不推荐):

js 复制代码
observer.observe({
  type: 'resource',
  buffered: true
});
参数说明:
  • entryTypes: 性能条目的类型(推荐)
  • type: 单一类型(不推荐)
  • buffered: 是否包括已存在的历史条目(true 会包含之前的记录)

五、支持的 entryType(性能条目类型)

类型 含义
resource 外部资源加载耗时
mark 用户自定义打点
measure 用户定义的测量点
paint 首次绘制(first-paint, first-contentful-paint)
navigation 页面导航
longtask 长任务(如 JS 卡顿)
element 关键可视元素曝光(需要配置)
largest-contentful-paint 最大内容绘制时间
layout-shift 布局偏移(CLS)

六、典型使用场景

  • 监听资源加载情况(如 img、script)
  • 监听 FCP、LCP、CLS、Long Tasks,用于 Web Vitals 性能分析
  • 异步获取自定义打点结果
  • 在 SPA 页面做性能埋点

七、注意事项

  • PerformanceObserver异步的:不会立即收到记录。
  • 使用 buffered: true 可获取已经发生的记录(旧数据),用于首次加载打点。
  • 页面进入后台或关闭时,需要调用 takeRecords() 收集剩余数据。
  • 一些条目需要在支持的浏览器中开启对应实验性特性(如 longtask)。

八、Mermaid 类图详解 PerformanceObserver

九、具体实现

javascript 复制代码
import { lazyReportBatch } from '../report';
const originalFetch = window.fetch;
function overwriteFetch() {
    window.fetch = function  newFetch(url, config) {
        const startTime = Date.now();
        const reportData = {
            type: 'performance',
            subType: 'fetch',
            url,
            startTime,
            method: config.method,
        }
        return originalFetch(url, config).then((res) => {
            const endTime = Date.now();
            reportData.endTime = endTime;
            reportData.duration = endTime - startTime;
            const data = res.clone();
            reportData.status = data.status;
            reportData.success = data.ok;
            // todo 上报数据
            lazyReportBatch(reportData);
            return res;
        }).catch((err) => {
            const endTime = Date.now();
            reportData.endTime = endTime;
            reportData.duration = endTime - startTime;
            reportData.status = 0;
            reportData.success = false;
            // todo 上报数据
            lazyReportBatch(reportData);
        });
    }
}
export default function fetch() {
    overwriteFetch();
}
javascript 复制代码
//监控FP
import { lazyReportBatch } from '../report';
export default function observerPaint() {
    const entryHandler = (list) => {
        for (const entry of list.getEntries()) {
            if (entry.name === 'first-paint') {
                observer.disconnect();
                const json = entry.toJSON();
                console.log(json);
                const reportData = {
                    ...json,
                    type: 'performance',
                    subType: entry.name,
                    pageUrl: window.location.href,
                }
                // 发送数据 todo;
                lazyReportBatch(reportData);
            }
        }
    
    }
    // 统计和计算fp的时间
    const observer = new PerformanceObserver(entryHandler);
    // buffered: true 确保观察到所有paint事件
    observer.observe({type: 'paint', buffered: true});
    
    
}
//监控FCP
import { lazyReportBatch } from '../report';
export default function observerFCP() {
    const entryHandler = (list) => {
        for (const entry of list.getEntries()) {
            if (entry.name === 'first-contentful-paint') {
                observer.disconnect();
                const json = entry.toJSON();
                console.log(json);
                const reportData = {
                    ...json,
                    type: 'performance',
                    subType: entry.name,
                    pageUrl: window.location.href,
                }
                // 发送数据 todo;
                lazyReportBatch(reportData);
            }
        }

    }
    // 统计和计算fcp的时间
    const observer = new PerformanceObserver(entryHandler);
    // buffered: true 确保观察到所有paint事件
    observer.observe({type: 'paint', buffered: true});
}
//监控LCP
import { lazyReportBatch } from '../report';
export default function observerLCP() {
    if (
        typeof PerformanceObserver === 'undefined' ||
        !PerformanceObserver.supportedEntryTypes.includes('largest-contentful-paint')
      ) {
        console.warn('LCP not supported in this browser.');
        return;
      }
    const entryHandler = (list,observer) => {
        if (observer) {
            observer.disconnect();
        } 
        for (const entry of list.getEntries()) {
            const json = entry.toJSON();
            //console.log(json);
            const reportData = {
                ...json,
                type: 'performance',
                subType: entry.name,
                pageUrl: window.location.href,
            }
            console.log(reportData);
            // 发送数据 todo;
            //lazyReportBatch(reportData);
        }

    }
    // 统计和计算lcp的时间
    const observer = new PerformanceObserver(entryHandler);
    // buffered: true 确保观察到所有paint事件
    observer.observe({type: 'largest-contentful-paint', buffered: true});
}
javascript 复制代码
import { lazyReportBatch } from '../report';
export default function observerLoad () {
    window.addEventListener('pageShow', function (event) {
        requestAnimationFrame(() =>{
            ['load'].forEach((type) => {
                const reportData = {
                    type: 'performance',
                    subType: type,
                    pageUrl: window.location.href,
                    startTime: performance.now()- event.timeStamp
                }
                // 发送数据
                lazyReportBatch(reportData);
            });

        }, true);
    });
}



import { lazyReportBatch } from '../report';
export const originalProto = XMLHttpRequest.prototype;
export const originalSend = originalProto.send;
export const originalOpen = originalProto.open;

function overwriteOpenAndSend() {
    originalProto.open = function newOpen(...args) {
        this.url = args[1];
        this.method = args[0];
        originalOpen.apply(this, args);

    }
    originalProto.send = function newSend(...args) {
        this.startTime = Date.now();
        const onLoaded = () => {
            this.endTime = Date.now();
            this.duration = this.endTime - this.startTime;
            const { url, method , startTime, endTime, duration, status} = this;
            const reportData = {
                status,
                duration,
                startTime,
                endTime,
                url,
                method: method.toUpperCase(),
                type: 'performance',
                success: status >= 200 && status < 300,
                subType: 'xhr'
            }
            // todo 发送数据
            lazyReportBatch(reportData);
            this.removeEventListener('loadend', onLoaded, true);
        }
        this.addEventListener('loadend', onLoaded, true);
        originalSend.apply(this, args);

    }

}
export default function xhr() {
    overwriteOpenAndSend();
}
相关推荐
袁煦丞5 分钟前
远程监控3D打印机的利器OctoPrint:cpolar内网穿透实验室第615个成功挑战
前端·程序员·远程工作
快乐就是哈哈哈11 分钟前
被问tsconfig.json 和 tsconfig.node.json 有什么作用,我懵了……
前端
feiyangqingyun17 分钟前
关于无法下载Qt离线安装包的说明
开发语言·qt
三体世界23 分钟前
Linux --TCP协议实现简单的网络通信(中英翻译)
linux·c语言·开发语言·网络·c++·windows·tcp/ip
苕皮蓝牙土豆24 分钟前
Qt信号与槽机制深度解析
开发语言·qt
苕皮蓝牙土豆27 分钟前
Qt概述:基础组件的使用
开发语言·qt
棉花糖超人28 分钟前
从【0-1的HTML】第1篇:HTML简介
前端·html
Hello-Mr.Wang34 分钟前
Vue3使用vue-web-screen-shot实现截图功能
前端·javascript·vue.js
zh_xuan41 分钟前
java Semaphore‌
java·开发语言