前端性能监控折腾手记
发版前 Lighthouse 跑分 95,产品经理反馈首页还是卡。本地实验室数据和用户真实网络完全是两回事,后面才补了 RUM,把 FCP、LCP 这些指标接到线上环境里看。
Lighthouse 监控
本地测试
# 使用 Chrome DevTools
# 或者使用命令行
npx lighthouse https://example.com --view
CI/CD 集成
# .github/workflows/performance.yml
name: Performance
on:
pull_request:
branches: [ main ]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run Lighthouse
uses: treosh/lighthouse-ci-action@v3
with:
urls: |
https://staging.example.com
uploadArtifacts: true
temporaryPublicStorage: true
性能预算
// package.json
{
"name": "myapp",
"scripts": {
"lighthouse": "lighthouse https://example.com --budget-path=budget.json --output=json"
}
}
// budget.json
[
{
"path": "build/*.js",
"timings": [
{
"metric": "first-contentful-paint",
"budget": 2000
},
{
"metric": "interactive",
"budget": 5000
}
],
"resourceSizes": [
{
"resourceType": "script",
"budget": 200
},
{
"resourceType": "stylesheet",
"budget": 50
}
]
}
]
Navigation Timing API
核心指标
function getNavigationTiming() {
const timing = performance.getEntriesByType('navigation')[0];
return {
// DNS 查询时间
dns: timing.domainLookupEnd - timing.domainLookupStart,
// TCP 连接时间
tcp: timing.connectEnd - timing.connectStart,
// 请求时间
request: timing.responseStart - timing.requestStart,
// 响应时间
response: timing.responseEnd - timing.responseStart,
// DOM 解析时间
dom: timing.domComplete - timing.domInteractive,
// 资源加载时间
resources: timing.loadEventStart - timing.domContentLoadedEventEnd,
// 总时间
total: timing.loadEventEnd - timing.navigationStart
};
}
Web Vitals
// First Contentful Paint (FCP)
const observerFCP = new PerformanceObserver((list) => {
const entries = list.getEntries();
const fcp = entries[0].startTime;
// 上报到监控系统
sendMetrics({ type: 'FCP', value: fcp });
observerFCP.disconnect();
});
observerFCP.observe({ type: 'paint', buffered: true });
// Largest Contentful Paint (LCP)
const observerLCP = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lcp = entries[entries.length - 1].startTime;
sendMetrics({ type: 'LCP', value: lcp });
});
observerLCP.observe({ type: 'largest-contentful-paint', buffered: true });
// First Input Delay (FID)
const observerFID = new PerformanceObserver((list) => {
const entries = list.getEntries();
const fid = entries[0].processingStart - entries[0].startTime;
sendMetrics({ type: 'FID', value: fid });
observerFID.disconnect();
});
observerFID.observe({ type: 'first-input', buffered: true });
// Cumulative Layout Shift (CLS)
let clsValue = 0;
let clsEntries = [];
const observerCLS = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value;
clsEntries.push(entry);
}
}
sendMetrics({ type: 'CLS', value: clsValue });
});
observerCLS.observe({ type: 'layout-shift', buffered: true });
Resource Timing API
资源性能
function getResourceTiming() {
const resources = performance.getEntriesByType('resource');
return resources.map(resource => ({
name: resource.name,
type: resource.initiatorType,
duration: resource.duration,
transferSize: resource.transferSize,
decodedBodySize: resource.decodedBodySize
}));
}
// 慢资源监控
function monitorSlowResources() {
const resources = performance.getEntriesByType('resource');
const slowResources = resources.filter(r => r.duration > 1000);
if (slowResources.length > 0) {
sendMetrics({
type: 'slow_resources',
resources: slowResources
});
}
}
// 监控错误资源
window.addEventListener('error', (event) => {
if (event.target !== window) {
sendMetrics({
type: 'resource_error',
resource: event.target.src || event.target.href,
tag: event.target.tagName
});
}
}, true);
Long Tasks API
长任务监控
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
sendMetrics({
type: 'long_task',
duration: entry.duration,
startTime: entry.startTime
});
}
});
observer.observe({ entryTypes: ['longtask'] });
错误监控
JavaScript 错误
window.addEventListener('error', (event) => {
sendMetrics({
type: 'js_error',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack
});
});
window.addEventListener('unhandledrejection', (event) => {
sendMetrics({
type: 'promise_rejection',
reason: event.reason
});
});
React 错误边界
class ErrorBoundary extends React.Component {
componentDidCatch(error, errorInfo) {
sendMetrics({
type: 'react_error',
error: error.toString(),
errorInfo: errorInfo.componentStack
});
}
render() {
return this.props.children;
}
}
用户行为监控
点击监控
document.addEventListener('click', (event) => {
const target = event.target;
sendMetrics({
type: 'click',
tag: target.tagName,
id: target.id,
className: target.className,
text: target.textContent?.slice(0, 50)
});
});
路由监控
// React Router
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function RouteMonitor() {
const location = useLocation();
useEffect(() => {
const startTime = performance.now();
return () => {
const duration = performance.now() - startTime;
sendMetrics({
type: 'route',
path: location.pathname,
duration
});
};
}, [location]);
return null;
}
监控数据上报
使用 sendBeacon
function sendMetrics(data) {
const blob = new Blob([JSON.stringify(data)], {
type: 'application/json'
});
navigator.sendBeacon('/api/metrics', blob);
}
使用 Image
function sendMetrics(data) {
const params = new URLSearchParams(data);
const img = new Image();
img.src = `/api/metrics?${params.toString()}`;
}
使用 XMLHttpRequest
function sendMetrics(data) {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/metrics', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(data));
}
监控数据处理
服务端处理
// Express 示例
app.use(express.json());
app.post('/api/metrics', async (req, res) => {
const metrics = req.body;
// 存储到数据库
await db.insert('metrics', {
type: metrics.type,
value: metrics.value,
userId: req.userId,
timestamp: new Date(),
userAgent: req.headers['user-agent'],
url: metrics.url
});
res.sendStatus(200);
});
数据聚合
// 按小时聚合
async function aggregateMetrics() {
const startTime = new Date(Date.now() - 3600000);
const endTime = new Date();
const metrics = await db.query(`
SELECT type, AVG(value) as avg_value, COUNT(*) as count
FROM metrics
WHERE timestamp BETWEEN ? AND ?
GROUP BY type
`, [startTime, endTime]);
// 存储聚合结果
await db.insert('metrics_hourly', {
hour: startTime,
data: JSON.stringify(metrics)
});
}
// 每小时执行一次
setInterval(aggregateMetrics, 3600000);
踩过的坑
坑一:采样率不当
一开始 100% 采样,数据太多,服务器扛不住。
解决:根据流量调整采样率。
const SAMPLING_RATE = 0.1; // 10% 采样
function sendMetrics(data) {
if (Math.random() > SAMPLING_RATE) {
return;
}
// 上报数据
...
}
坑二:敏感信息泄露
监控数据中包含了用户信息。
解决:过滤敏感信息。
function sanitizeData(data) {
const sensitiveKeys = ['password', 'token', 'ssn', 'creditCard'];
const sanitized = { ...data };
for (const key in sanitized) {
for (const sensitiveKey of sensitiveKeys) {
if (key.toLowerCase().includes(sensitiveKey.toLowerCase())) {
sanitized[key] = '***REDACTED***';
}
}
}
return sanitized;
}
坑三:网络环境影响
用户网络环境差异大,监控数据不准确。
解决:记录网络信息,分组分析。
function getNetworkInfo() {
if (navigator.connection) {
return {
type: navigator.connection.effectiveType,
downlink: navigator.connection.downlink,
rtt: navigator.connection.rtt
};
}
return null;
}
function sendMetrics(data) {
const networkInfo = getNetworkInfo();
return {
...data,
network: networkInfo
};
}
写在最后
前端性能监控这东西,不是技术问题,是用户体验问题。
监控了:
- 页面加载时间
- 交互响应时间
- 错误率
- 用户行为
带来了:
- 运维成本
- 数据分析成本
- 隐私问题
实施之前先评估:
- 业务需求
- 用户规模
- 预算
- 隐私要求
不是所有监控指标都重要,先关注影响用户体验的关键指标。
这次前端性能监控改造花了三周,从 Lighthouse 到 RUM。改造完成后,问题发现时间从 24 小时降到 1 小时,用户体验提升明显。
版权声明: 本文首发于 指尖魔法屋-前端性能监控折腾手记(https://blog.thinkmoon.cn/post/71-frontend-performance-monitoring-lighthouse-rum/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。