前端性能监控是确保用户获得流畅体验、及时发现和解决性能瓶颈的关键环节。它涉及收集用户在真实环境中与应用交互时的性能数据。
前端性能监控指标
前端性能指标可以分为几大类:
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 运行时错误:
luawindow.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 阻止浏览器默认的错误处理 };
-
资源加载错误:
csharpwindow.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 未捕获的拒绝:
csharpwindow.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
示例)
-
import { getCLS, getFID, getLCP, getTTFB, getFCP } from 'web-vitals';
:- 从
web-vitals
库中导入了五个函数,每个函数对应一个性能指标的收集。 - 这些函数会自动处理
PerformanceObserver
的注册、监听和数据处理,大大简化了代码。
- 从
-
const reportWebVitals = (onPerfEntry) => { ... };
:- 定义了一个名为
reportWebVitals
的函数,它接受一个回调函数onPerfEntry
作为参数。 - 这个回调函数将在每个性能指标被收集到时被调用。
- 定义了一个名为
-
if (onPerfEntry && onPerfEntry instanceof Function) { ... }
:- 一个简单的检查,确保传入的回调函数是有效的。
-
getCLS(onPerfEntry); getFID(onPerfEntry); getLCP(onPerfEntry); getTTFB(onPerfEntry); getFCP(onPerfEntry);
:- 分别调用这些函数,并传入
onPerfEntry
回调。 - 当这些指标的数据准备好时,
web-vitals
库会自动调用onPerfEntry
,并传入一个metric
对象作为参数。
- 分别调用这些函数,并传入
-
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')。
-
-
sendPerformanceMetric(metric);
:- 在
onPerfEntry
回调中,调用sendPerformanceMetric
函数,将收集到的metric
对象发送到后端。
- 在
-
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
这样的库,你可以构建一个全面而高效的前端性能监控系统。