核心问题与技术挑战
现代 React 应用随着业务复杂度增加,性能问题和运行时错误日益成为影响用户体验的关键因素。没有可靠的监控与错误上报机制,我们将陷入被动修复而非主动预防的困境。
性能指标体系与错误分类
关键性能指标定义
jsx
// performance-metrics.js
export const PERFORMANCE_METRICS = {
// 页面加载指标
FP: 'first-paint', // 首次绘制
FCP: 'first-contentful-paint', // 首次内容绘制
LCP: 'largest-contentful-paint', // 最大内容绘制
FID: 'first-input-delay', // 首次输入延迟
TTI: 'time-to-interactive', // 可交互时间
TBT: 'total-blocking-time', // 总阻塞时间
CLS: 'cumulative-layout-shift', // 累积布局偏移
// React 特有指标
COMPONENT_RENDER_TIME: 'component-render-time', // 组件渲染时间
EFFECT_TIME: 'effect-execution-time', // Effect执行时间
RERENDER_COUNT: 'component-rerender-count', // 组件重渲染次数
CONTEXT_CHANGES: 'context-change-frequency', // Context变更频率
MEMO_HIT_RATE: 'memo-hit-rate', // memo命中率
};
错误分类与等级定义
jsx
// error-classification.js
export const ERROR_TYPES = {
RENDER_ERROR: 'render-error', // 渲染错误
EFFECT_ERROR: 'effect-error', // 副作用错误
EVENT_HANDLER_ERROR: 'event-error', // 事件处理错误
ASYNC_ERROR: 'async-error', // 异步操作错误
RESOURCE_LOADING_ERROR: 'resource-error', // 资源加载错误
API_ERROR: 'api-error', // API请求错误
UNCAUGHT_ERROR: 'uncaught-error', // 未捕获的错误
};
export const ERROR_LEVELS = {
FATAL: 'fatal', // 致命错误:导致应用崩溃或核心功能无法使用
ERROR: 'error', // 错误:功能无法正常工作,但不影响整体应用
WARNING: 'warning', // 警告:可能存在问题,但功能仍可使用
INFO: 'info', // 信息:值得注意的异常状况但无功能影响
};
监控工具选型与评估
Ran tool
监控工具对比分析
工具名称 | 类型 | 性能监控能力 | 错误捕获 | 集成复杂度 | 数据所有权 | 成本结构 | 适用场景 |
---|---|---|---|---|---|---|---|
React Profiler | 内置 | 中(组件级) | 无 | 低 | 完全自有 | 免费 | 开发调试 |
Performance API | 内置 | 高 | 无 | 中 | 完全自有 | 免费 | 核心指标采集 |
Sentry | 第三方 | 中 | 强 | 低 | 外部存储 | 免费起步,按量付费 | 中小型应用 |
LogRocket | 第三方 | 高 | 强 | 低 | 外部存储 | 付费 | 用户体验分析 |
自建系统 | 自研 | 可定制 | 可定制 | 高 | 完全自有 | 开发+维护成本 | 大型复杂应用 |
自定义监控系统架构设计
jsx
// 项目结构
/src
/monitoring
/performance
metrics-collector.js
render-tracker.js
interaction-tracker.js
/errors
error-boundary.js
error-handler.js
api-error-tracker.js
/reporting
data-aggregator.js
transport-layer.js
batch-processor.js
/config
sampling-rules.js
metrics-thresholds.js
index.js
性能监控核心实现
性能数据采集器
jsx
// metrics-collector.js
import { PERFORMANCE_METRICS } from '../config/metrics-definitions';
class PerformanceMetricsCollector {
metrics = new Map();
markTimestamps = new Map();
mark(name) {
this.markTimestamps.set(name, performance.now());
// 兼容 performance.mark API
if (performance.mark) {
performance.mark(`${name}-start`);
}
}
measure(name, startMark) {
if (!this.markTimestamps.has(startMark)) {
console.warn(`Start mark "${startMark}" not found for measure "${name}"`);
return;
}
const startTime = this.markTimestamps.get(startMark);
const duration = performance.now() - startTime;
// 记录此次测量值
if (!this.metrics.has(name)) {
this.metrics.set(name, []);
}
this.metrics.get(name).push(duration);
// 兼容 performance.measure API
if (performance.measure) {
try {
performance.measure(name, `${startMark}-start`);
} catch (e) {
// 某些浏览器在mark不存在时会抛出异常
}
}
return duration;
}
getMetrics(name) {
if (!this.metrics.has(name)) return null;
const values = this.metrics.get(name);
return {
name,
values,
min: Math.min(...values),
max: Math.max(...values),
avg: values.reduce((sum, val) => sum + val, 0) / values.length,
median: this.calculateMedian(values),
p95: this.calculatePercentile(values, 95),
};
}
getAllMetrics() {
const result = {};
this.metrics.forEach((values, name) => {
result[name] = this.getMetrics(name);
});
return result;
}
calculateMedian(values) {
if (!values.length) return 0;
const sorted = [...values].sort((a, b) => a - b);
const middle = Math.floor(sorted.length / 2);
return sorted.length % 2 === 0
? (sorted[middle - 1] + sorted[middle]) / 2
: sorted[middle];
}
calculatePercentile(values, percentile) {
if (!values.length) return 0;
const sorted = [...values].sort((a, b) => a - b);
const index = Math.ceil((percentile / 100) * sorted.length) - 1;
return sorted[index];
}
// Web Vitals指标采集
captureWebVitals() {
const { onLCP, onFID, onCLS, onTTFB } = require('web-vitals');
onLCP(({ value }) => {
if (!this.metrics.has(PERFORMANCE_METRICS.LCP)) {
this.metrics.set(PERFORMANCE_METRICS.LCP, []);
}
this.metrics.get(PERFORMANCE_METRICS.LCP).push(value);
this.reportMetric(PERFORMANCE_METRICS.LCP, value);
});
onFID(({ value }) => {
if (!this.metrics.has(PERFORMANCE_METRICS.FID)) {
this.metrics.set(PERFORMANCE_METRICS.FID, []);
}
this.metrics.get(PERFORMANCE_METRICS.FID).push(value);
this.reportMetric(PERFORMANCE_METRICS.FID, value);
});
onCLS(({ value }) => {
if (!this.metrics.has(PERFORMANCE_METRICS.CLS)) {
this.metrics.set(PERFORMANCE_METRICS.CLS, []);
}
this.metrics.get(PERFORMANCE_METRICS.CLS).push(value);
this.reportMetric(PERFORMANCE_METRICS.CLS, value);
});
onTTFB(({ value }) => {
if (!this.metrics.has('TTFB')) {
this.metrics.set('TTFB', []);
}
this.metrics.get('TTFB').push(value);
this.reportMetric('TTFB', value);
});
}
reportMetric(name, value) {
// 将指标发送到上报系统
if (this.reporter) {
this.reporter.sendMetric({
name,
value,
timestamp: Date.now()
});
}
}
setReporter(reporter) {
this.reporter = reporter;
}
}
export const metricsCollector = new PerformanceMetricsCollector();
export default metricsCollector;
React 组件性能追踪 HOC
jsx
// render-tracker.js
import React, { Component } from 'react';
import metricsCollector from './metrics-collector';
import { PERFORMANCE_METRICS } from '../config/metrics-definitions';
// 追踪组件渲染性能的高阶组件
export function withRenderTracking(WrappedComponent, options = {}) {
const {
trackProps = false,
trackState = false,
componentName = WrappedComponent.displayName || WrappedComponent.name || 'Component',
threshold = 16, // 默认阈值16ms (60fps)
} = options;
return class RenderTracker extends Component {
static displayName = `RenderTracker(${componentName})`;
// 记录重渲染次数
rerenderCount = 0;
// 用于记录渲染前props和state
prevProps = null;
prevState = null;
componentDidMount() {
this.recordMounting();
}
shouldComponentUpdate(nextProps, nextState) {
this.prevProps = this.props;
this.prevState = this.state;
return true;
}
componentDidUpdate() {
this.rerenderCount++;
this.recordRerender();
if (trackProps && this.prevProps) {
const changedProps = this.getChangedProps(this.prevProps, this.props);
if (Object.keys(changedProps).length > 0) {
this.recordPropChanges(changedProps);
}
}
if (trackState && this.prevState) {
const changedState = this.getChangedProps(this.prevState, this.state);
if (Object.keys(changedState).length > 0) {
this.recordStateChanges(changedState);
}
}
}
getChangedProps(prev, current) {
const changes = {};
Object.keys(current).forEach(key => {
if (prev[key] !== current[key]) {
changes[key] = {
from: prev[key],
to: current[key]
};
}
});
return changes;
}
recordMounting() {
const renderTime = metricsCollector.measure(
`${componentName}-mount`,
`${componentName}-render-start`
);
if (renderTime > threshold) {
console.warn(
`[Performance] Component ${componentName} took ${renderTime.toFixed(2)}ms to mount, ` +
`which exceeds the threshold of ${threshold}ms.`
);
}
metricsCollector.reportMetric(
PERFORMANCE_METRICS.COMPONENT_RENDER_TIME,
{ componentName, phase: 'mount', duration: renderTime }
);
}
recordRerender() {
const renderTime = metricsCollector.measure(
`${componentName}-rerender-${this.rerenderCount}`,
`${componentName}-render-start`
);
if (renderTime > threshold) {
console.warn(
`[Performance] Component ${componentName} took ${renderTime.toFixed(2)}ms to rerender, ` +
`which exceeds the threshold of ${threshold}ms. (Rerender #${this.rerenderCount})`
);
}
metricsCollector.reportMetric(
PERFORMANCE_METRICS.COMPONENT_RENDER_TIME,
{ componentName, phase: 'update', count: this.rerenderCount, duration: renderTime }
);
metricsCollector.reportMetric(
PERFORMANCE_METRICS.RERENDER_COUNT,
{ componentName, count: this.rerenderCount }
);
}
recordPropChanges(changedProps) {
metricsCollector.reportMetric(
'prop-changes',
{ componentName, changes: changedProps }
);
}
recordStateChanges(changedState) {
metricsCollector.reportMetric(
'state-changes',
{ componentName, changes: changedState }
);
}
render() {
metricsCollector.mark(`${componentName}-render-start`);
return <WrappedComponent {...this.props} />;
}
};
}
// 针对函数组件的性能追踪Hook
export function useRenderTracking(componentName, options = {}) {
const {
threshold = 16
} = options;
const renderCount = React.useRef(0);
React.useEffect(() => {
const renderTime = metricsCollector.measure(
`${componentName}-render-${renderCount.current}`,
`${componentName}-render-start`
);
if (renderTime > threshold) {
console.warn(
`[Performance] Component ${componentName} took ${renderTime.toFixed(2)}ms to render, ` +
`which exceeds the threshold of ${threshold}ms. (Render #${renderCount.current})`
);
}
metricsCollector.reportMetric(
PERFORMANCE_METRICS.COMPONENT_RENDER_TIME,
{ componentName, count: renderCount.current, duration: renderTime }
);
renderCount.current++;
});
// 在组件渲染之前标记
React.useLayoutEffect(() => {
metricsCollector.mark(`${componentName}-render-start`);
}, [componentName]);
return renderCount.current;
}
错误监控与上报系统
全局错误边界组件
jsx
// error-boundary.js
import React, { Component } from 'react';
import { ERROR_TYPES, ERROR_LEVELS } from '../config/error-classification';
import errorReporter from './error-reporter';
export class ErrorBoundary extends Component {
static defaultProps = {
fallback: null,
onError: null,
errorLevel: ERROR_LEVELS.ERROR,
componentName: 'Unknown',
};
state = {
hasError: false,
error: null,
errorInfo: null
};
componentDidCatch(error, errorInfo) {
const { componentName, errorLevel, onError } = this.props;
// 更新组件状态
this.setState({
hasError: true,
error,
errorInfo
});
// 获取组件树结构
const componentStack = errorInfo?.componentStack || '';
// 构造错误信息
const errorData = {
type: ERROR_TYPES.RENDER_ERROR,
level: errorLevel,
message: error.message,
stack: error.stack,
componentStack,
componentName,
time: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent,
// 错误的额外上下文
context: {
route: window.location.pathname,
...this.props.errorContext
}
};
// 上报错误
errorReporter.reportError(errorData);
// 调用父组件错误处理函数
if (typeof onError === 'function') {
onError(error, errorInfo);
}
// 记录到控制台
console.error('[ErrorBoundary]', error, errorInfo);
}
resetError = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null
});
};
render() {
const { fallback, children } = this.props;
const { hasError, error, errorInfo } = this.state;
if (hasError) {
// 提供重置错误的方法给fallback组件
const resetHandler = {
resetError: this.resetError
};
// 如果提供了fallback组件
if (fallback) {
if (React.isValidElement(fallback)) {
return React.cloneElement(fallback, {
error,
errorInfo,
...resetHandler
});
} else if (typeof fallback === 'function') {
return fallback({
error,
errorInfo,
...resetHandler
});
}
}
// 默认错误UI
return (
<div className="error-boundary-fallback">
<h2>组件渲染出错</h2>
<p>
抱歉,组件渲染出现了问题。请尝试刷新页面或联系技术支持。
</p>
<button onClick={this.resetError}>重试</button>
</div>
);
}
return children;
}
}
// 高阶组件封装
export function withErrorBoundary(Component, options = {}) {
const {
fallback,
onError,
errorLevel = ERROR_LEVELS.ERROR,
errorContext = {}
} = options;
const componentName = Component.displayName || Component.name || 'Unknown';
const WrappedComponent = (props) => (
<ErrorBoundary
fallback={fallback}
onError={onError}
errorLevel={errorLevel}
componentName={componentName}
errorContext={{
...errorContext,
props: Object.keys(props)
}}
>
<Component {...props} />
</ErrorBoundary>
);
WrappedComponent.displayName = `withErrorBoundary(${componentName})`;
return WrappedComponent;
}
全局错误捕获服务
jsx
// error-handler.js
import { ERROR_TYPES, ERROR_LEVELS } from '../config/error-classification';
class ErrorHandler {
constructor(reporter) {
this.reporter = reporter;
this.initialized = false;
this.ignorePatterns = [
// 忽略一些非关键或第三方错误
/Script error\./i,
/ResizeObserver loop limit exceeded/i,
];
}
initialize() {
if (this.initialized) return;
// 捕获未处理的Promise异常
window.addEventListener('unhandledrejection', this.handlePromiseRejection);
// 捕获全局JavaScript错误
window.addEventListener('error', this.handleWindowError, true);
// 拦截console.error (可选)
if (this.options?.interceptConsoleError) {
this.originalConsoleError = console.error;
console.error = (...args) => {
this.handleConsoleError(...args);
this.originalConsoleError.apply(console, args);
};
}
this.initialized = true;
console.log('[ErrorHandler] Global error handler initialized');
}
setOptions(options = {}) {
this.options = {
captureUnhandledRejections: true,
captureGlobalErrors: true,
interceptConsoleError: false,
samplingRate: 1.0, // 1.0 = 捕获所有错误
maxErrorsPerMinute: 10,
...options
};
}
setReporter(reporter) {
this.reporter = reporter;
}
handleWindowError = (event) => {
// 阻止浏览器默认错误处理
event.preventDefault();
const { message, filename, lineno, colno, error } = event;
// 检查是否应忽略此错误
if (this.shouldIgnoreError(message)) {
return true;
}
// 构造错误信息
const errorData = {
type: ERROR_TYPES.UNCAUGHT_ERROR,
level: ERROR_LEVELS.FATAL,
message,
stack: error?.stack || '',
source: {
file: filename,
line: lineno,
column: colno
},
time: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent
};
// 上报错误
this.reportError(errorData);
return true;
};
handlePromiseRejection = (event) => {
// 组装有意义的错误信息
const error = event.reason;
const message = error?.message || 'Unhandled Promise Rejection';
// 检查是否应忽略此错误
if (this.shouldIgnoreError(message)) {
return;
}
const errorData = {
type: ERROR_TYPES.ASYNC_ERROR,
level: ERROR_LEVELS.ERROR,
message,
stack: error?.stack || '',
time: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent
};
// 上报错误
this.reportError(errorData);
};
handleConsoleError = (...args) => {
// 提取有意义的错误信息
const errorMessage = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
).join(' ');
// 检查是否应忽略此错误
if (this.shouldIgnoreError(errorMessage)) {
return;
}
const errorData = {
type: ERROR_TYPES.CONSOLE_ERROR,
level: ERROR_LEVELS.WARNING,
message: errorMessage.slice(0, 500), // 限制长度
time: Date.now(),
url: window.location.href
};
// 上报错误
this.reportError(errorData);
};
shouldIgnoreError(message) {
// 检查是否匹配忽略模式
return this.ignorePatterns.some(pattern => pattern.test(message));
}
addIgnorePattern(pattern) {
if (pattern instanceof RegExp) {
this.ignorePatterns.push(pattern);
} else if (typeof pattern === 'string') {
this.ignorePatterns.push(new RegExp(pattern, 'i'));
}
}
reportError(errorData) {
// 采样控制
if (Math.random() > this.options?.samplingRate) {
return;
}
// 限流控制
if (this.isRateLimited()) {
return;
}
// 使用上报服务发送错误
if (this.reporter) {
this.reporter.sendError(errorData);
}
}
// 实现错误上报频率限制
isRateLimited() {
const now = Date.now();
const maxPerMinute = this.options?.maxErrorsPerMinute || 10;
if (!this._errorTimestamps) {
this._errorTimestamps = [];
}
// 清理一分钟前的错误记录
this._errorTimestamps = this._errorTimestamps.filter(
timestamp => now - timestamp < 60000
);
// 检查是否超出限制
if (this._errorTimestamps.length >= maxPerMinute) {
return true;
}
// 记录当前错误时间戳
this._errorTimestamps.push(now);
return false;
}
// 清理资源
destroy() {
if (!this.initialized) return;
window.removeEventListener('unhandledrejection', this.handlePromiseRejection);
window.removeEventListener('error', this.handleWindowError, true);
if (this.originalConsoleError) {
console.error = this.originalConsoleError;
}
this.initialized = false;
}
}
export const errorHandler = new ErrorHandler();
export default errorHandler;
API 错误跟踪器
jsx
// api-error-tracker.js
import { ERROR_TYPES, ERROR_LEVELS } from '../config/error-classification';
// 创建拦截器以追踪API请求错误
export function createAPIErrorTracker(reporter) {
// Fetch API拦截
const originalFetch = window.fetch;
window.fetch = async function trackedFetch(url, options = {}) {
const startTime = performance.now();
const requestId = generateRequestId();
// 捕获请求信息
const requestInfo = {
url: typeof url === 'string' ? url : url.url,
method: options.method || 'GET',
requestId,
startTime
};
try {
// 记录请求开始
reporter.sendMetric({
name: 'api-request-start',
value: requestInfo
});
// 执行原始请求
const response = await originalFetch.apply(this, arguments);
// 计算请求时间
const duration = performance.now() - startTime;
// 处理非2xx响应
if (!response.ok) {
let responseBody = '';
try {
// 克隆响应以便仍可读取主体
const clonedResponse = response.clone();
responseBody = await clonedResponse.text();
} catch (e) {
// 无法读取响应体
responseBody = 'Unable to read response body';
}
// 上报API错误
const errorData = {
type: ERROR_TYPES.API_ERROR,
level: response.status >= 500 ? ERROR_LEVELS.ERROR : ERROR_LEVELS.WARNING,
message: `API Error: ${response.status} ${response.statusText}`,
time: Date.now(),
url: window.location.href,
context: {
request: {
url: requestInfo.url,
method: requestInfo.method,
requestId
},
response: {
status: response.status,
statusText: response.statusText,
body: responseBody.substring(0, 1000) // 限制大小
},
duration
}
};
reporter.sendError(errorData);
}
// 记录请求完成
reporter.sendMetric({
name: 'api-request-complete',
value: {
...requestInfo,
status: response.status,
duration,
success: response.ok
}
});
return response;
} catch (error) {
// 计算请求时间
const duration = performance.now() - startTime;
// 上报网络错误
const errorData = {
type: ERROR_TYPES.API_ERROR,
level: ERROR_LEVELS.ERROR,
message: `Network Error: ${error.message}`,
stack: error.stack,
time: Date.now(),
url: window.location.href,
context: {
request: {
url: requestInfo.url,
method: requestInfo.method,
requestId
},
error: error.message,
duration
}
};
reporter.sendError(errorData);
// 记录请求失败
reporter.sendMetric({
name: 'api-request-failed',
value: {
...requestInfo,
error: error.message,
duration,
success: false
}
});
// 重新抛出原始错误
throw error;
}
};
// Axios拦截器(如果项目使用Axios)
if (window.axios) {
window.axios.interceptors.request.use(config => {
config.requestId = generateRequestId();
config.startTime = performance.now();
// 记录请求开始
reporter.sendMetric({
name: 'api-request-start',
value: {
url: config.url,
method: config.method,
requestId: config.requestId,
startTime: config.startTime
}
});
return config;
});
window.axios.interceptors.response.use(
response => {
const { config } = response;
const duration = performance.now() - config.startTime;
// 记录请求完成
reporter.sendMetric({
name: 'api-request-complete',
value: {
url: config.url,
method: config.method,
requestId: config.requestId,
status: response.status,
duration,
success: true
}
});
return response;
},
error => {
const { config, response } = error;
// 某些情况下请求可能未发出
if (!config) {
return Promise.reject(error);
}
const duration = performance.now() - config.startTime;
// 上报API错误
const errorData = {
type: ERROR_TYPES.API_ERROR,
level: response?.status >= 500 ? ERROR_LEVELS.ERROR : ERROR_LEVELS.WARNING,
message: `API Error: ${response?.status || 'Network Error'} ${error.message}`,
time: Date.now(),
url: window.location.href,
context: {
request: {
url: config.url,
method: config.method,
requestId: config.requestId
},
response: response ? {
status: response.status,
statusText: response.statusText,
data: response.data
} : null,
duration
}
};
reporter.sendError(errorData);
// 记录请求失败
reporter.sendMetric({
name: 'api-request-failed',
value: {
url: config.url,
method: config.method,
requestId: config.requestId,
error: error.message,
status: response?.status,
duration,
success: false
}
});
return Promise.reject(error);
}
);
}
// 生成请求ID
function generateRequestId() {
return `req_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
}
return {
// 恢复原始方法
restore: () => {
window.fetch = originalFetch;
if (window.axios) {
window.axios.interceptors.request.eject(0);
window.axios.interceptors.response.eject(0);
}
}
};
}
数据上报与聚合系统
上报传输层
jsx
// transport-layer.js
class DataTransport {
constructor(options = {}) {
this.options = {
endpoint: '/monitoring/collect',
batchSize: 10,
flushInterval: 5000, // 5秒
retryAttempts: 3,
retryDelay: 1000,
useBeacon: true,
...options
};
this.buffer = [];
this.isSending = false;
this.flushTimer = null;
this.retryQueue = [];
// 启动定期刷新
this.startPeriodicFlush();
// 页面卸载时发送所有待处理数据
window.addEventListener('beforeunload', this.flushBeforeUnload);
}
setEndpoint(endpoint) {
this.options.endpoint = endpoint;
}
send(data) {
// 添加通用字段
const enrichedData = {
...data,
timestamp: data.timestamp || Date.now(),
session: this.getSessionInfo(),
user: this.getUserInfo(),
app: this.getAppInfo()
};
// 添加到缓冲区
this.buffer.push(enrichedData);
// 如果达到批处理大小,立即发送
if (this.buffer.length >= this.options.batchSize) {
this.flush();
}
return true;
}
async flush() {
if (this.isSending || this.buffer.length === 0) {
return;
}
this.isSending = true;
// 提取当前缓冲区数据
const dataToSend = [...this.buffer];
this.buffer = [];
try {
// 尝试发送数据
const success = await this.sendData(dataToSend);
if (!success) {
// 如果发送失败,将数据添加到重试队列
this.addToRetryQueue(dataToSend);
}
} catch (error) {
console.error('[Monitoring] Error sending monitoring data:', error);
// 发送失败,添加到重试队列
this.addToRetryQueue(dataToSend);
}
this.isSending = false;
// 处理重试队列
if (this.retryQueue.length > 0) {
this.processRetryQueue();
}
}
async sendData(data) {
// 检查页面是否正在卸载
if (this.isUnloading && this.options.useBeacon && navigator.sendBeacon) {
// 使用Beacon API发送数据(更可靠地处理页面卸载场景)
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
return navigator.sendBeacon(this.options.endpoint, blob);
} else {
// 使用标准fetch API
try {
const response = await fetch(this.options.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
// 不跟随重定向
redirect: 'error',
// 发送凭据(例如cookies)
credentials: 'same-origin',
// 设置较短的超时时间
signal: AbortSignal.timeout(10000) // 10秒超时
});
return response.ok;
} catch (error) {
console.error('[Monitoring] Transport error:', error);
return false;
}
}
}
addToRetryQueue(data) {
// 添加到重试队列,并记录重试次数
this.retryQueue.push({
data,
attempts: 0,
nextRetry: Date.now() + this.options.retryDelay
});
}
async processRetryQueue() {
if (this.isProcessingRetryQueue) {
return;
}
this.isProcessingRetryQueue = true;
const now = Date.now();
const itemsToRetry = this.retryQueue.filter(item => item.nextRetry <= now);
// 更新重试队列,删除将要重试的项目
this.retryQueue = this.retryQueue.filter(item => item.nextRetry > now);
for (const item of itemsToRetry) {
// 增加重试次数
item.attempts++;
try {
const success = await this.sendData(item.data);
if (!success && item.attempts < this.options.retryAttempts) {
// 计算下一次重试时间(使用指数退避)
const nextRetryDelay = this.options.retryDelay * Math.pow(2, item.attempts - 1);
item.nextRetry = Date.now() + nextRetryDelay;
// 重新添加到队列
this.retryQueue.push(item);
}
} catch (error) {
if (item.attempts < this.options.retryAttempts) {
// 计算下一次重试时间
const nextRetryDelay = this.options.retryDelay * Math.pow(2, item.attempts - 1);
item.nextRetry = Date.now() + nextRetryDelay;
// 重新添加到队列
this.retryQueue.push(item);
}
}
}
this.isProcessingRetryQueue = false;
}
startPeriodicFlush() {
// 定期刷新缓冲区
this.flushTimer = setInterval(() => {
if (this.buffer.length > 0) {
this.flush();
}
// 处理重试队列
if (this.retryQueue.length > 0) {
this.processRetryQueue();
}
}, this.options.flushInterval);
}
flushBeforeUnload = () => {
// 标记正在卸载,这会使sendData使用navigator.beacon
this.isUnloading = true;
// 取消计时器
clearInterval(this.flushTimer);
// 合并重试队列和当前缓冲区
const allPendingData = [
...this.buffer,
...this.retryQueue.map(item => item.data).flat()
];
if (allPendingData.length > 0) {
// 使用同步方式发送所有数据
const blob = new Blob([JSON.stringify(allPendingData)], { type: 'application/json' });
navigator.sendBeacon(this.options.endpoint, blob);
}
};
getSessionInfo() {
// 在实际应用中,应该从会话管理系统获取这些信息
return {
id: window.sessionStorage.getItem('session_id') || 'unknown',
startedAt: parseInt(window.sessionStorage.getItem('session_start') || Date.now()),
pageViews: parseInt(window.sessionStorage.getItem('page_views') || '1')
};
}
getUserInfo() {
// 在实际应用中,应该从身份验证系统获取这些信息
// 注意:确保遵守隐私法规和公司政策
return {
// 使用匿名ID或经过同意的用户标识符
id: window.localStorage.getItem('user_id') || 'anonymous',
// 可以添加经过许可的用户属性
type: window.localStorage.getItem('user_type') || 'visitor'
};
}
getAppInfo() {
return {
name: window.APP_NAME || document.title,
version: window.APP_VERSION || '1.0',
environment: window.APP_ENV || process.env.NODE_ENV || 'production',
reactVersion: React.version,
viewportSize: `${window.innerWidth}x${window.innerHeight}`,
language: navigator.language,
platform: navigator.platform
};
}
destroy() {
// 清理资源
clearInterval(this.flushTimer);
window.removeEventListener('beforeunload', this.flushBeforeUnload);
// 发送所有待处理数据
if (this.buffer.length > 0 || this.retryQueue.length > 0) {
this.flushBeforeUnload();
}
}
}
export const dataTransport = new DataTransport();
export default dataTransport;
数据聚合与批处理器
jsx
// batch-processor.js
import dataTransport from './transport-layer';
class MonitoringReporter {
constructor(transport) {
this.transport = transport;
this.metricsBuffer = {};
this.errorBuffer = [];
this.flushInterval = 10000; // 10秒
this.bufferSize = {
metrics: 20,
errors: 5
};
// 启动定期批处理
this.startPeriodicFlush();
}
// 发送性能指标
sendMetric(metric) {
const { name, value } = metric;
// 按指标类型进行分组
if (!this.metricsBuffer[name]) {
this.metricsBuffer[name] = [];
}
// 添加时间戳
const metricWithTimestamp = {
...metric,
timestamp: metric.timestamp || Date.now()
};
// 添加到缓冲区
this.metricsBuffer[name].push(metricWithTimestamp);
// 如果该类型的指标达到阈值,就发送此类型的所有指标
if (this.metricsBuffer[name].length >= this.bufferSize.metrics) {
this.flushMetricsByType(name);
}
return true;
}
// 发送错误
sendError(error) {
// 添加到错误缓冲区
this.errorBuffer.push({
...error,
timestamp: error.timestamp || Date.now()
});
// 如果错误达到阈值,立即发送
if (this.errorBuffer.length >= this.bufferSize.errors) {
this.flushErrors();
}
return true;
}
// 按指标类型刷新缓冲区
flushMetricsByType(metricType) {
if (!this.metricsBuffer[metricType] || this.metricsBuffer[metricType].length === 0) {
return;
}
// 提取要发送的指标
const metricsToSend = [...this.metricsBuffer[metricType]];
// 清空缓冲区
this.metricsBuffer[metricType] = [];
// 构造批量数据包
const payload = {
type: 'metric',
metricType,
data: metricsToSend
};
// 发送到传输层
this.transport.send(payload);
}
// 刷新所有错误
flushErrors() {
if (this.errorBuffer.length === 0) {
return;
}
// 提取要发送的错误
const errorsToSend = [...this.errorBuffer];
// 清空缓冲区
this.errorBuffer = [];
// 构造批量数据包
const payload = {
type: 'error',
data: errorsToSend
};
// 发送到传输层
this.transport.send(payload);
}
// 刷新所有指标
flushAllMetrics() {
// 遍历所有指标类型
Object.keys(this.metricsBuffer).forEach(metricType => {
if (this.metricsBuffer[metricType].length > 0) {
this.flushMetricsByType(metricType);
}
});
}
// 刷新所有数据
flushAll() {
this.flushAllMetrics();
this.flushErrors();
}
// 启动定期刷新
startPeriodicFlush() {
this.flushTimer = setInterval(() => {
this.flushAll();
}, this.flushInterval);
// 页面隐藏时刷新数据
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.flushAll();
}
});
}
// 设置缓冲区大小
setBufferSize(type, size) {
if (type === 'metrics' || type === 'errors') {
this.bufferSize[type] = size;
}
}
// 销毁实例,清理资源
destroy() {
clearInterval(this.flushTimer);
this.flushAll();
}
}
export const reporter = new MonitoringReporter(dataTransport);
export default reporter;
系统集成与自动化配置
监控系统初始化
jsx
// index.js
import React from 'react';
import { PERFORMANCE_METRICS } from './config/metrics-definitions';
import { ERROR_LEVELS } from './config/error-classification';
import metricsCollector from './performance/metrics-collector';
import { withRenderTracking, useRenderTracking } from './performance/render-tracker';
import { ErrorBoundary, withErrorBoundary } from './errors/error-boundary';
import errorHandler from './errors/error-handler';
import { createAPIErrorTracker } from './errors/api-error-tracker';
import reporter from './reporting/batch-processor';
import dataTransport from './reporting/transport-layer';
// 默认配置
const defaultConfig = {
enablePerformanceMonitoring: true,
enableErrorMonitoring: true,
errorReportingEndpoint: '/api/error-reporting',
metricsReportingEndpoint: '/api/metrics-reporting',
samplingRate: 0.1, // 采样 10% 的用户
logLevel: ERROR_LEVELS.ERROR, // 仅报告错误及以上级别
maxErrorsPerMinute: 10,
captureConsoleErrors: false,
enableReactProfiling: false,
};
// 监控系统主类
class ReactMonitoring {
constructor() {
this.initialized = false;
this.config = { ...defaultConfig };
}
init(userConfig = {}) {
if (this.initialized) {
console.warn('[ReactMonitoring] Already initialized. Call destroy() first if you need to reinitialize.');
return this;
}
// 合并用户配置
this.config = {
...defaultConfig,
...userConfig,
};
// 随机采样决定是否为这个用户启用监控
const shouldMonitor = Math.random() < this.config.samplingRate;
if (!shouldMonitor) {
console.log('[ReactMonitoring] This session was not selected for monitoring (sampling)');
return this;
}
// 配置数据传输层
dataTransport.setEndpoint(this.config.errorReportingEndpoint);
// 初始化错误处理
if (this.config.enableErrorMonitoring) {
this.initErrorMonitoring();
}
// 初始化性能监控
if (this.config.enablePerformanceMonitoring) {
this.initPerformanceMonitoring();
}
this.initialized = true;
console.log('[ReactMonitoring] Initialized successfully');
return this;
}
initErrorMonitoring() {
// 配置错误处理器
errorHandler.setOptions({
captureUnhandledRejections: true,
captureGlobalErrors: true,
interceptConsoleError: this.config.captureConsoleErrors,
samplingRate: 1.0, // 捕获所有发生的错误
maxErrorsPerMinute: this.config.maxErrorsPerMinute,
});
// 设置错误上报服务
errorHandler.setReporter(reporter);
// 初始化全局错误处理
errorHandler.initialize();
// 创建API错误跟踪器
this.apiErrorTracker = createAPIErrorTracker(reporter);
console.log('[ReactMonitoring] Error monitoring initialized');
}
initPerformanceMonitoring() {
// 配置指标收集器
metricsCollector.setReporter(reporter);
// 捕获Web Vitals
metricsCollector.captureWebVitals();
// 捕获首次加载性能
this.captureInitialPerformance();
if (this.config.enableReactProfiling && window.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
this.setupReactProfiling();
}
console.log('[ReactMonitoring] Performance monitoring initialized');
}
captureInitialPerformance() {
// 利用Performance Timeline API捕获关键性能指标
if (window.performance && performance.timing) {
// 等待加载完成
if (document.readyState === 'complete') {
this.processPerformanceTiming();
} else {
window.addEventListener('load', () => {
// 延迟一下以确保所有指标都已可用
setTimeout(() => this.processPerformanceTiming(), 0);
});
}
}
}
processPerformanceTiming() {
const timing = performance.timing;
// 计算关键性能指标
const metrics = {
// DNS解析时间
dns: timing.domainLookupEnd - timing.domainLookupStart,
// TCP连接时间
tcp: timing.connectEnd - timing.connectStart,
// 请求时间
request: timing.responseStart - timing.requestStart,
// 响应时间
response: timing.responseEnd - timing.responseStart,
// DOM解析时间
domParse: timing.domInteractive - timing.responseEnd,
// DOM内容加载
domContentLoaded: timing.domContentLoadedEventEnd - timing.domContentLoadedEventStart,
// DOM完全加载
domComplete: timing.domComplete - timing.domLoading,
// 页面完全加载时间
pageLoad: timing.loadEventEnd - timing.navigationStart,
// 首次渲染时间(近似)
firstPaint: timing.domLoading - timing.navigationStart,
};
// 上报指标
Object.entries(metrics).forEach(([name, value]) => {
reporter.sendMetric({
name: `page_${name}`,
value,
category: 'page-load',
});
});
}
setupReactProfiling() {
// 这需要React DevTools扩展的钩子
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
if (hook && hook.supportsFiber) {
// 获取React实例
const renderers = hook.getFiberRoots ? hook.getFiberRoots(1) : null;
if (renderers) {
for (const renderer of renderers) {
// 添加分析器
if (hook.onCommitFiberRoot) {
const originalOnCommitFiberRoot = hook.onCommitFiberRoot.bind(hook);
hook.onCommitFiberRoot = (...args) => {
const [, root] = args;
try {
this.processReactCommit(root);
} catch (e) {
console.error('[ReactMonitoring] Error in React profiling:', e);
}
// 调用原始方法
return originalOnCommitFiberRoot(...args);
};
}
}
}
}
}
processReactCommit(root) {
// 这是一个简化版的实现
// 实际上,从Fiber树提取性能数据很复杂,需要深入了解React内部工作原理
try {
const commitTime = performance.now();
reporter.sendMetric({
name: PERFORMANCE_METRICS.COMPONENT_RENDER_TIME,
value: {
commitTime,
components: this.extractComponentInfo(root)
}
});
} catch (e) {
console.error('[ReactMonitoring] Failed to process React commit:', e);
}
}
extractComponentInfo(root) {
// 这是一个简化的实现
// 在实际应用中,需要遍历Fiber树来提取组件信息
return {
timestamp: performance.now(),
// 这里应该有更多组件特定的数据
};
}
// 提供React组件和钩子
getReactComponents() {
return {
ErrorBoundary,
withErrorBoundary,
withRenderTracking,
useRenderTracking,
};
}
// 清理和销毁监控系统
destroy() {
if (!this.initialized) {
return;
}
// 清理错误处理
if (errorHandler) {
errorHandler.destroy();
}
// 清理API错误跟踪
if (this.apiErrorTracker) {
this.apiErrorTracker.restore();
}
// 清理数据上报
if (reporter) {
reporter.destroy();
}
if (dataTransport) {
dataTransport.destroy();
}
this.initialized = false;
console.log('[ReactMonitoring] System destroyed and cleaned up');
}
}
// 创建单例实例
export const reactMonitoring = new ReactMonitoring();
// 导出React组件和钩子,方便直接使用
export const {
ErrorBoundary,
withErrorBoundary,
withRenderTracking,
useRenderTracking,
} = reactMonitoring.getReactComponents();
// 默认导出监控系统实例
export default reactMonitoring;
应用实践
应用集成示例
jsx
// 在应用入口 index.js 中初始化
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import reactMonitoring from './monitoring';
// 初始化监控系统
reactMonitoring.init({
enablePerformanceMonitoring: true,
enableErrorMonitoring: true,
errorReportingEndpoint: 'https://api.example.com/monitoring/errors',
metricsReportingEndpoint: 'https://api.example.com/monitoring/metrics',
samplingRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0, // 生产环境采样10%,开发环境全采样
});
// 使用全局错误边界包装应用
const MonitoredApp = () => (
<reactMonitoring.ErrorBoundary
fallback={<div>应用出现了问题,正在尝试恢复...</div>}
errorLevel="fatal"
>
<App />
</reactMonitoring.ErrorBoundary>
);
ReactDOM.render(<MonitoredApp />, document.getElementById('root'));
组件级性能监控示例
jsx
// 使用HOC监控类组件
import React, { Component } from 'react';
import { withRenderTracking, withErrorBoundary } from './monitoring';
class ExpensiveComponent extends Component {
render() {
return (
<div>
{/* 复杂组件逻辑 */}
{this.props.items.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
}
// 应用监控HOC
export default withErrorBoundary(
withRenderTracking(ExpensiveComponent, {
componentName: 'ExpensiveComponent',
threshold: 8, // 8ms渲染警告阈值
trackProps: true
}),
{
fallback: <div>组件加载失败</div>,
errorLevel: 'error'
}
);
// 使用Hook监控函数组件
import React, { useState } from 'react';
import { useRenderTracking } from './monitoring';
function ExpensiveCounter(props) {
// 监控组件渲染性能
const renderCount = useRenderTracking('ExpensiveCounter', { threshold: 5 });
const [count, setCount] = useState(0);
// 模拟一个可能导致性能问题的操作
const expensiveCalculation = () => {
// 假设这是一个昂贵的计算
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += i;
}
return result;
};
const result = expensiveCalculation();
return (
<div>
<p>Count: {count}</p>
<p>Calculation: {result}</p>
<p>Render count: {renderCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default ExpensiveCounter;
监控数据可视化方案
jsx
// 监控仪表板组件
import React, { useState, useEffect } from 'react';
import { LineChart, BarChart, PieChart } from 'some-chart-library';
import { fetchMetricsData, fetchErrorData } from '../api';
export function PerformanceDashboard() {
const [metrics, setMetrics] = useState(null);
const [errors, setErrors] = useState(null);
const [timeRange, setTimeRange] = useState('24h');
const [loading, setLoading] = useState(true);
useEffect(() => {
async function loadData() {
setLoading(true);
try {
// 并行加载数据
const [metricsData, errorsData] = await Promise.all([
fetchMetricsData({ timeRange }),
fetchErrorData({ timeRange })
]);
setMetrics(metricsData);
setErrors(errorsData);
} catch (err) {
console.error('Failed to load monitoring data:', err);
} finally {
setLoading(false);
}
}
loadData();
}, [timeRange]);
if (loading) {
return <div>Loading dashboard data...</div>;
}
// 渲染性能指标图表
return (
<div className="monitoring-dashboard">
<div className="dashboard-header">
<h1>React Application Monitoring</h1>
<div className="time-selector">
<button onClick={() => setTimeRange('1h')}>Last Hour</button>
<button onClick={() => setTimeRange('24h')}>Last 24 Hours</button>
<button onClick={() => setTimeRange('7d')}>Last 7 Days</button>
</div>
</div>
<div className="dashboard-section">
<h2>Core Web Vitals</h2>
<div className="metrics-grid">
<MetricCard
title="LCP"
value={metrics.lcp.median}
threshold={2500}
unit="ms"
description="Largest Contentful Paint"
/>
<MetricCard
title="FID"
value={metrics.fid.median}
threshold={100}
unit="ms"
description="First Input Delay"
/>
<MetricCard
title="CLS"
value={metrics.cls.median}
threshold={0.1}
unit=""
description="Cumulative Layout Shift"
/>
</div>
<h3>LCP Trend</h3>
<LineChart
data={metrics.lcp.history}
xKey="timestamp"
yKey="value"
threshold={2500}
/>
</div>
<div className="dashboard-section">
<h2>Component Performance</h2>
<BarChart
data={metrics.componentRenderTime}
xKey="componentName"
yKey="avgDuration"
sortBy="avgDuration"
/>
</div>
<div className="dashboard-section">
<h2>Error Distribution</h2>
<PieChart
data={errors.byType}
valueKey="count"
labelKey="type"
/>
<h3>Recent Errors</h3>
<ErrorsTable errors={errors.recent} />
</div>
</div>
);
}
// 单个指标卡片组件
function MetricCard({ title, value, threshold, unit, description }) {
// 根据阈值确定状态
const getStatus = () => {
if (value <= threshold * 0.75) return 'good';
if (value <= threshold) return 'warning';
return 'poor';
};
const status = getStatus();
return (
<div className={`metric-card ${status}`}>
<div className="metric-title">{title}</div>
<div className="metric-value">
{value.toFixed(2)}{unit}
</div>
<div className="metric-description">{description}</div>
<div className="metric-threshold">
{status === 'good' && '✓ Good'}
{status === 'warning' && '⚠️ Needs Improvement'}
{status === 'poor' && '✗ Poor'}
</div>
</div>
);
}
// 错误表格组件
function ErrorsTable({ errors }) {
return (
<table className="errors-table">
<thead>
<tr>
<th>Time</th>
<th>Type</th>
<th>Message</th>
<th>Component</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{errors.map(error => (
<tr key={error.id}>
<td>{new Date(error.timestamp).toLocaleString()}</td>
<td>{error.type}</td>
<td>{error.message}</td>
<td>{error.componentName || 'N/A'}</td>
<td>
<button onClick={() => viewErrorDetails(error.id)}>Details</button>
</td>
</tr>
))}
</tbody>
</table>
);
}
性能优化建议与实施方案
基于监控收集的数据,我们可以制定针对性的优化策略:
-
组件懒加载与代码分割:根据页面加载性能数据,识别首屏加载瓶颈,将非关键组件延迟加载。
-
状态管理优化 :利用渲染追踪数据,识别过度渲染的组件,应用
React.memo
、useMemo
和useCallback
降低不必要的重渲染。 -
虚拟化长列表:对于识别出渲染时间过长的列表组件,应用窗口化技术(react-window)仅渲染可视区域项目。
-
图片与资源优化:根据资源加载错误和性能数据,优化静态资源加载策略,应用懒加载与适当的分辨率。
-
错误预防机制:基于收集的错误模式构建更健壮的输入验证和错误恢复机制,提高应用稳定性。
总结与反思
构建完整的 React 性能监控与错误上报系统需要系统性地考虑数据采集、传输、存储和分析等环节。我们应该遵循以下原则:
-
低侵入性:通过 HOC 和 Hooks 模式,实现了对组件的无痛监控,不影响业务逻辑。
-
可扩展性:模块化设计使系统易于根据实际需求进行扩展和定制。
-
性能影响最小化:采样策略和批处理机制确保监控系统本身不会成为应用的性能负担。
-
数据安全与隐私:提供了匿名化和数据过滤机制,符合现代数据保护要求。
-
自动化分析:通过阈值检测和趋势分析,实现了问题的自动识别与预警。
在实际应用中,还应根据项目规模和需求选择合适的集成方式,从简单的单一指标监控开始,逐步扩展到全面的性能与错误追踪系统,以持续提升 React 应用的质量与用户体验。
参考资源
官方文档与规范
- React 性能优化文档 - React 官方性能优化指南
- Web Vitals - Google 定义的核心网页指标标准
- Performance API - MDN 关于浏览器 Performance API 的详细文档
- Error Boundaries - React 官方错误边界文档
- React Profiler API - React 内置性能分析 API 文档
监控工具与框架
- Sentry - 功能丰富的错误监控平台,提供 React SDK
- LogRocket - 会话重放和前端监控系统
- New Relic - 全栈性能监控解决方案
- Datadog RUM - 真实用户监控平台
开源库
- Web Vitals - 测量核心 Web 指标的小型库
- React Query - 包含自动错误处理功能的数据管理库
- React Error Boundary - 灵活的错误边界组件
- why-did-you-render - 检测不必要的组件重渲染
- use-error-boundary - 用于函数组件的错误边界 Hook
服务端集成
- OpenTelemetry - 开源可观测性框架,适用于分布式跟踪
- Elasticsearch + Kibana - 强大的日志分析和可视化工具
- Grafana - 开源指标分析与可视化平台
- Prometheus - 开源系统监控和告警工具
技术博客与最佳实践
- Netflix 技术博客: 性能监控 - Netflix 的前端性能监控实践
- Airbnb 的前端错误跟踪实践
- Facebook 工程博客: 前端性能
- 前端观察性工程实践
- React 性能:Stack Overflow 内部实践
行业标准与测量工具
- Lighthouse - 网站质量自动化审计工具
- WebPageTest - 免费网站性能测试工具
- Chrome DevTools Performance - 深入性能分析指南
- React Developer Tools Profiler - React 专用性能分析工具
如果你觉得这篇文章有帮助,欢迎点赞收藏,也期待在评论区看到你的想法和建议!👇
终身学习,共同成长。
咱们下一期见
💻