前端性能监控笔记

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

前端性能监控指标

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

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.js 或 src/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 这样的库,你可以构建一个全面而高效的前端性能监控系统。

相关推荐
lovingsoft24 分钟前
Chrome Lighthouse 详解
前端·chrome
Qlly34 分钟前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端
马可家的菠萝38 分钟前
你的微信“文件传输助手”,是不是已经成了第二个网盘?
前端·后端·架构
略略略啦38 分钟前
Vibe Coding 基于Spec DD和TDD的实践指南
前端
shmily麻瓜小菜鸡1 小时前
前端实现图片与PDF文字识别敏感信息打码方案
前端·pdf·状态模式
打工仔折腾 AI1 小时前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战
汉堡大王95271 小时前
单次输出拉到100万Token:拆完 Gemini 4 Argon 的续写协议,才看懂谷歌这7个月在憋什么
前端·人工智能·后端
看浪的路人1 小时前
第9讲:大模型应用监控与可观测性——OpenTelemetry + Prometheus + Grafana 全链路追踪
java·前端·spring
内存过载1 小时前
用表格模拟甘特图:项目排期的土办法
前端·低代码
葡萄城技术团队2 小时前
用 SpreadJS 数据验证实现省市区三级联动
前端