前端性能监控笔记

前端性能监控是确保用户获得流畅体验、及时发现和解决性能瓶颈的关键环节。它涉及收集用户在真实环境中与应用交互时的性能数据。

前端性能监控指标

前端性能指标可以分为几大类:

1. 加载性能指标 (Loading Performance)

衡量页面加载速度和用户看到内容的速度。

  • TTFB (Time to First Byte) : 从用户发起请求到浏览器接收到服务器响应的第一个字节的时间。

    • 意义: 反映服务器处理请求和网络传输的效率。
  • FCP (First Contentful Paint) : 首次内容绘制。页面上任何内容(文本、图片、非空白 Canvas/SVG)首次渲染到屏幕上的时间。

    • 意义: 用户感知到页面开始加载的时间点。
  • LCP (Largest Contentful Paint) : 最大内容绘制。衡量页面主要内容加载完成的时间,即视口内最大的内容元素(图片、视频、文本块)渲染完成的时间。

    • 意义: 核心 Web 指标之一,反映用户感知到的主要内容加载速度。
  • DCL (DOMContentLoaded) : DOMContentLoaded 事件触发的时间。HTML 文档已被完全加载和解析,不包括样式表、图片等外部资源。

    • 意义: 页面 DOM 结构已准备好,可以开始执行 JS 逻辑。
  • Load Event (onLoad) : window.onload 事件触发的时间。页面上所有资源(包括图片、样式表、脚本等)都已加载完成。

    • 意义: 页面所有内容都已加载完毕,用户可以完全交互。

2. 交互性能指标 (Interactivity Performance)

衡量用户与页面交互的响应速度。

  • FID (First Input Delay) : 首次输入延迟。从用户第一次与页面交互(如点击按钮、输入文本)到浏览器实际能够响应这些交互的时间。

    • 意义: 核心 Web 指标之一,反映页面对用户输入的响应能力。
  • TBT (Total Blocking Time) : 总阻塞时间。衡量 FCP 到 TTI 之间主线程被阻塞的总时长。

    • 意义: 页面在加载过程中无法响应用户输入的时间总和。
  • TTI (Time to Interactive) : 可交互时间。页面达到完全可交互状态所需的时间。

    • 意义: 页面视觉上已渲染完成,初始脚本已加载,并且能够快速响应用户输入。

3. 视觉稳定性指标 (Visual Stability)

衡量页面布局在加载过程中的稳定性。

  • CLS (Cumulative Layout Shift) : 累计布局偏移。衡量页面在加载过程中发生的意外布局偏移的总和。

    • 意义: 核心 Web 指标之一,反映页面内容的视觉稳定性,避免用户在点击时发生内容跳动。

4. 运行时性能指标 (Runtime Performance)

衡量页面在用户使用过程中的流畅度。

  • FPS (Frames Per Second) : 帧率。衡量页面动画和滚动是否流畅。
  • Long Tasks: 耗时任务。主线程上执行时间超过 50 毫秒的任务。
  • Memory Usage: 内存使用。页面占用的内存大小,防止内存泄漏。

5. 自定义指标 (Custom Metrics)

根据业务需求定义的特定指标。

  • Time to First Meaningful Paint: 首次有意义绘制时间(例如,主要内容区域渲染完成)。
  • Time to First Data: 页面数据首次渲染到屏幕上的时间。
  • API 请求耗时: 关键 API 接口的响应时间。

如何实现监测 (原生 API)

现代浏览器提供了强大的 Performance API 来获取这些指标。

1. 使用 PerformanceObserver API (推荐用于核心 Web 指标)

PerformanceObserver 允许你订阅性能事件,并在这些事件发生时收到通知。这是获取 LCP, FID, CLS 等指标的推荐方式。

php 复制代码
// 示例:监听 LCP
const observerLCP = new PerformanceObserver((entryList) => {
    for (const entry of entryList.getEntries()) {
        // LCP 可能是多个,取最后一个最新的
        const lcp = entry.startTime;
        console.log('LCP:', lcp);
        // 在这里发送数据到你的后端
        sendPerformanceData({ name: 'LCP', value: lcp });
    }
});
observerLCP.observe({ type: 'largest-contentful-paint', buffered: true }); // buffered: true 确保能获取到页面加载前的数据

// 示例:监听 CLS
const observerCLS = new PerformanceObserver((entryList) => {
    let cls = 0;
    for (const entry of entryList.getEntries()) {
        if (!entry.hadRecentInput) { // 排除用户输入导致的布局偏移
            cls += entry.value;
        }
    }
    console.log('CLS:', cls);
    sendPerformanceData({ name: 'CLS', value: cls });
});
observerCLS.observe({ type: 'layout-shift', buffered: true });

// 示例:监听 FID (需要用户交互后才能触发)
const observerFID = new PerformanceObserver((entryList) => {
    for (const entry of entryList.getEntries()) {
        const fid = entry.processingStart - entry.startTime;
        console.log('FID:', fid);
        sendPerformanceData({ name: 'FID', value: fid });
        observerFID.disconnect(); // FID 只记录第一次,获取后即可断开
    }
});
observerFID.observe({ type: 'first-input', buffered: true });

2. 使用 performance.timing (已废弃,但仍可用)

performance.timing 提供了页面加载各个阶段的时间戳。

javascript 复制代码
window.addEventListener('load', () => {
    setTimeout(() => { // 确保所有性能数据都已记录
        const timing = performance.timing;

        // TTFB (Time to First Byte)
        const ttfb = timing.responseStart - timing.requestStart;
        console.log('TTFB:', ttfb);
        sendPerformanceData({ name: 'TTFB', value: ttfb });

        // DCL (DOMContentLoaded)
        const dcl = timing.domContentLoadedEventEnd - timing.navigationStart;
        console.log('DCL:', dcl);
        sendPerformanceData({ name: 'DCL', value: dcl });

        // Load Event (onLoad)
        const onLoad = timing.loadEventEnd - timing.navigationStart;
        console.log('onLoad:', onLoad);
        sendPerformanceData({ name: 'onLoad', value: onLoad });

        // FCP (First Contentful Paint) - 需要特殊处理,performance.timing 不直接提供
        // FCP 可以通过 PerformanceObserver 或 performance.getEntriesByType('paint') 获取
        const paintEntries = performance.getEntriesByType('paint');
        const fcpEntry = paintEntries.find(entry => entry.name === 'first-contentful-paint');
        if (fcpEntry) {
            const fcp = fcpEntry.startTime;
            console.log('FCP:', fcp);
            sendPerformanceData({ name: 'FCP', value: fcp });
        }

    }, 0); // 延迟执行,确保所有事件都已触发
});

3. 资源加载时间 (performance.getEntriesByType('resource'))

用于监控页面中所有资源的加载时间。

javascript 复制代码
window.addEventListener('load', () => {
    setTimeout(() => {
        const resources = performance.getEntriesByType('resource');
        resources.forEach(resource => {
            console.log(`Resource: ${resource.name}, Duration: ${resource.duration}`);
            // 可以根据资源类型、大小等进行筛选和上报
            sendPerformanceData({
                name: 'ResourceLoad',
                value: resource.duration,
                resourceName: resource.name,
                resourceType: resource.initiatorType
            });
        });
    }, 0);
});

4. 自定义埋点 (performance.mark() / performance.measure())

用于测量代码中特定阶段的耗时。

javascript 复制代码
performance.mark('start_custom_logic');

// 你的自定义逻辑代码
// ... 模拟耗时操作
for (let i = 0; i < 1000000; i++) { Math.sqrt(i); }
// ...

performance.mark('end_custom_logic');
performance.measure('custom_logic_duration', 'start_custom_logic', 'end_custom_logic');

const measures = performance.getEntriesByName('custom_logic_duration');
if (measures.length > 0) {
    console.log('Custom Logic Duration:', measures[0].duration);
    sendPerformanceData({ name: 'CustomLogic', value: measures[0].duration });
}

5. 错误监控

  • JS 运行时错误:

    lua 复制代码
    window.onerror = function(message, source, lineno, colno, error) {
        console.error('JS Error:', message, source, lineno, colno, error);
        sendErrorData({ type: 'js_error', message, source, lineno, colno, stack: error ? error.stack : '' });
        return true; // 返回 true 阻止浏览器默认的错误处理
    };
  • 资源加载错误:

    csharp 复制代码
    window.addEventListener('error', (event) => {
        if (event.target && (event.target.tagName === 'IMG' || event.target.tagName === 'LINK' || event.target.tagName === 'SCRIPT')) {
            console.error('Resource Error:', event.target.src || event.target.href);
            sendErrorData({ type: 'resource_error', src: event.target.src || event.target.href, tagName: event.target.tagName });
        }
    }, true); // 使用捕获阶段
  • Promise 未捕获的拒绝:

    csharp 复制代码
    window.addEventListener('unhandledrejection', (event) => {
        console.error('Unhandled Promise Rejection:', event.reason);
        sendErrorData({ type: 'promise_rejection', reason: event.reason ? event.reason.stack || event.reason.message || event.reason : 'unknown' });
    });

6. 数据上报函数

javascript 复制代码
function sendPerformanceData(data) {
    // 实际项目中,你会将数据发送到你的后端服务或第三方监控平台
    // 使用 navigator.sendBeacon 可以确保数据在页面卸载时也能发送
    if (navigator.sendBeacon) {
        navigator.sendBeacon('/api/performance-metrics', JSON.stringify(data));
    } else {
        // Fallback for older browsers
        fetch('/api/performance-metrics', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data)
        }).catch(e => console.error('Failed to send performance data:', e));
    }
}

function sendErrorData(data) {
    if (navigator.sendBeacon) {
        navigator.sendBeacon('/api/error-logs', JSON.stringify(data));
    } else {
        fetch('/api/error-logs', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data)
        }).catch(e => console.error('Failed to send error data:', e));
    }
}

如果用库如何实现监测 (以 web-vitals 为例)

使用库可以极大地简化性能指标的收集和上报。Google 官方的 web-vitals 库是专门用于收集核心 Web 指标的轻量级工具。

1. 安装 web-vitals

csharp 复制代码
npm install web-vitals
# 或者
yarn add web-vitals

2. 实现监测代码

创建一个单独的性能监控文件,例如 src/reportWebVitals.js

scss 复制代码
// src/reportWebVitals.js
import { getCLS, getFID, getLCP, getTTFB, getFCP } from 'web-vitals';

const reportWebVitals = (onPerfEntry) => {
  if (onPerfEntry && onPerfEntry instanceof Function) {
    getCLS(onPerfEntry);
    getFID(onPerfEntry);
    getLCP(onPerfEntry);
    getTTFB(onPerfEntry); // TTFB 和 FCP 不是核心 Web Vitals,但 web-vitals 库也提供了
    getFCP(onPerfEntry);
  }
};

export default reportWebVitals;

3. 在应用入口文件中使用

在你的应用入口文件(例如 src/index.jssrc/main.js)中导入并调用 reportWebVitals

javascript 复制代码
// src/index.js (React App 示例)
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals'; // 导入监控文件

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// 调用 reportWebVitals,并将数据发送到你的后端
reportWebVitals(metric => {
  console.log(metric); // 打印收集到的指标数据

  // 在这里将 metric 对象发送到你的后端服务或监控平台
  // 例如:
  sendPerformanceMetric(metric);
});

function sendPerformanceMetric(metric) {
  // 实际项目中,你需要将这些数据发送到你的服务器或第三方 RUM 平台
  // 可以批量发送,或者根据需要立即发送
  const data = {
    name: metric.name,      // 指标名称 (e.g., 'LCP', 'FID', 'CLS')
    value: metric.value,    // 指标值
    id: metric.id,          // 唯一ID,用于聚合数据
    delta: metric.delta,    // 对于 CLS,表示从上次报告以来的变化
    rating: metric.rating,  // 'good', 'needs-improvement', 'bad'
    navigationType: metric.navigationType, // 'navigate', 'reload', 'back-forward', 'prerender'
    // 可以添加更多上下文信息,如:
    // url: window.location.href,
    // userId: 'user_id_here',
    // sessionId: 'session_id_here',
    // timestamp: Date.now()
  };

  if (navigator.sendBeacon) {
    navigator.sendBeacon('/api/web-vitals', JSON.stringify(data));
  } else {
    fetch('/api/web-vitals', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    }).catch(e => console.error('Failed to send web-vitals data:', e));
  }
}

详细代码讲解 (web-vitals 示例)

  1. import { getCLS, getFID, getLCP, getTTFB, getFCP } from 'web-vitals'; :

    • web-vitals 库中导入了五个函数,每个函数对应一个性能指标的收集。
    • 这些函数会自动处理 PerformanceObserver 的注册、监听和数据处理,大大简化了代码。
  2. const reportWebVitals = (onPerfEntry) => { ... }; :

    • 定义了一个名为 reportWebVitals 的函数,它接受一个回调函数 onPerfEntry 作为参数。
    • 这个回调函数将在每个性能指标被收集到时被调用。
  3. if (onPerfEntry && onPerfEntry instanceof Function) { ... } :

    • 一个简单的检查,确保传入的回调函数是有效的。
  4. getCLS(onPerfEntry); getFID(onPerfEntry); getLCP(onPerfEntry); getTTFB(onPerfEntry); getFCP(onPerfEntry); :

    • 分别调用这些函数,并传入 onPerfEntry 回调。
    • 当这些指标的数据准备好时,web-vitals 库会自动调用 onPerfEntry,并传入一个 metric 对象作为参数。
  5. reportWebVitals(metric => { ... }); (在 index.js 中) :

    • 这是实际调用 reportWebVitals 的地方。

    • 传入的箭头函数就是 onPerfEntry 回调。

    • 每次 web-vitals 库收集到一个指标(例如 LCP 值、FID 值等)时,这个箭头函数就会被执行。

    • metric 对象包含了收集到的性能指标的详细信息,例如:

      • metric.name: 指标的名称,如 'LCP', 'FID', 'CLS'。
      • metric.value: 指标的数值。
      • metric.id: 一个唯一的 ID,用于在后续分析中识别和聚合数据。
      • metric.delta: 对于 CLS,表示从上次报告以来的变化量。
      • metric.rating: Google 建议的指标评级,如 'good', 'needs-improvement', 'bad'。
      • metric.navigationType: 页面加载的类型(如 'navigate', 'reload')。
  6. sendPerformanceMetric(metric); :

    • onPerfEntry 回调中,调用 sendPerformanceMetric 函数,将收集到的 metric 对象发送到后端。
  7. sendPerformanceMetric 函数:

    • 这个函数负责将数据实际发送到你的服务器。
    • 它首先构建一个包含必要信息的 data 对象。
    • navigator.sendBeacon : 这是发送性能数据到后端的推荐方式。它会在浏览器空闲时异步发送数据,并且即使页面即将卸载(用户关闭标签页或导航到其他页面),也能保证数据发送成功,而不会阻塞主线程或影响页面卸载速度。
    • fetch : 如果 sendBeacon 不可用(例如在一些旧浏览器中),则回退到 fetch API。fetch 也是异步的,但它在页面卸载时不如 sendBeacon 稳定。

生产环境注意事项

  • 数据采样: 在高流量网站上,你可能不需要收集每个用户的每次访问数据。可以考虑进行数据采样(例如,只收集 1% 或 10% 的用户数据),以减少服务器负载和数据存储成本。
  • 数据隐私: 确保你收集的数据不包含任何个人身份信息 (PII)。如果需要,对数据进行匿名化处理。
  • 监控脚本的影响: 监控脚本本身会消耗资源。确保你的监控代码是轻量级的,并且不会对页面性能造成负面影响。将监控脚本异步加载或延迟加载。
  • 后端分析和可视化: 收集到的数据需要一个强大的后端系统来存储、处理、分析和可视化。你可以使用现成的 RUM (Real User Monitoring) 解决方案(如 Sentry, DataDog, New Relic, Dynatrace)或自建系统。
  • 报警系统: 设置阈值报警,当关键性能指标超出预期时,及时通知开发团队。

通过结合原生 API 和 web-vitals 这样的库,你可以构建一个全面而高效的前端性能监控系统。

相关推荐
Data_Adventure15 分钟前
Vite 项目中使用 vite-plugin-dts 插件的详细指南
前端·vue.js
八戒社18 分钟前
如何使用插件和子主题添加WordPress自定义CSS(附:常见错误)
前端·css·tensorflow·wordpress
xzboss30 分钟前
DOM转矢量PDF
前端·javascript
一无所有不好吗30 分钟前
纯前端vue项目实现版本更新(纯代码教程)
前端
安全系统学习44 分钟前
内网横向之RDP缓存利用
前端·安全·web安全·网络安全·中间件
Hilaku1 小时前
为什么我不再相信 Tailwind?三个月重构项目教会我的事
前端·css·前端框架
FogLetter1 小时前
JavaScript 的历史:从网页点缀到改变世界的编程语言
前端·javascript·http
鹏北海1 小时前
Vue3+TS的H5项目实现微信分享卡片样式
前端·微信
轻颂呀1 小时前
进程——环境变量及程序地址空间
前端·chrome
lyc2333331 小时前
鸿蒙Stage模型:轻量高效的应用架构「舞台革命」🎭
前端