前端监控系统
⚠️ 本文档部分内容已过时,正在更新中。请参考最新官方文档获取最新信息。
更新时间:2025-02
目录导航
什么是前端监控
前端监控是指对前端应用的性能、错误、用户行为等进行实时监控和分析的系统。
核心目标:
- 发现和定位问题
- 优化用户体验
- 提升应用质量
- 数据驱动决策
监控类型:
- 性能监控:页面加载、资源加载、接口请求
- 错误监控:JS 错误、资源错误、接口错误
- 行为监控:用户行为、页面访问、点击事件
监控指标
1. 性能指标
Web Vitals 核心指标:
⚠️ 重要更新:2024 年 3 月起,INP (Interaction to Next Paint) 已正式取代 FID (First Input Delay) 成为 Core Web Vitals 核心指标。FID 仅衡量首次输入延迟,而 INP 衡量用户与页面所有交互的响应性,更能反映真实用户体验。
javascript
// LCP (Largest Contentful Paint) - 最大内容绘制
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('LCP:', entry.renderTime || entry.loadTime)
}
}).observe({ entryTypes: ['largest-contentful-paint'] })
// INP (Interaction to Next Paint) - 交互到下次绘制(2024年3月取代FID成为核心指标)
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// INP = 从用户交互到下次绘制的延迟
console.log('INP:', entry.duration)
}
}).observe({ type: 'event', buffered: true })
// FCP (First Contentful Paint) - 首次内容绘制
// FMP (First Meaningful Paint) - 首次有意义绘制
// TTI (Time to Interactive) - 可交互时间
// TBT (Total Blocking Time) - 总阻塞时间
// CLS (Cumulative Layout Shift) - 累积布局偏移
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry.name, entry.startTime)
}
})
observer.observe({ entryTypes: ['paint', 'navigation', 'resource'] })2. 错误指标
javascript
// JS 错误数量
// 资源加载错误数量
// 接口错误数量
// 错误率 = 错误数 / 总请求数3. 用户行为指标
javascript
// PV (Page View) - 页面浏览量
// UV (Unique Visitor) - 独立访客数
// 页面停留时间
// 跳出率
// 点击率性能监控
1. 页面加载性能
javascript
class PerformanceMonitor {
constructor() {
this.init()
}
init() {
if (window.PerformanceObserver) {
this.observePerformance()
} else {
window.addEventListener('load', () => {
this.collectPerformance()
})
}
}
observePerformance() {
// 监听导航时间(使用 Navigation Timing Level 2 API)
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.reportNavigation(entry)
}
}).observe({ entryTypes: ['navigation'] })
// 监听资源加载
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.reportResource(entry)
}
}).observe({ entryTypes: ['resource'] })
// 监听 Web Vitals
this.observeWebVitals()
}
collectPerformance() {
// ⚠️ performance.timing 已废弃,推荐使用 Navigation Timing Level 2 API
// 旧写法(已废弃):
// const timing = performance.timing
// 新写法:
const [navEntry] = performance.getEntriesByType('navigation')
if (!navEntry) return
const data = {
// DNS 查询时间
dns: navEntry.domainLookupEnd - navEntry.domainLookupStart,
// TCP 连接时间
tcp: navEntry.connectEnd - navEntry.connectStart,
// SSL 握手时间
ssl: navEntry.secureConnectionStart ? navEntry.connectEnd - navEntry.secureConnectionStart : 0,
// 请求时间
request: navEntry.responseStart - navEntry.requestStart,
// 响应时间
response: navEntry.responseEnd - navEntry.responseStart,
// DOM 解析时间
domParse: navEntry.domInteractive - navEntry.responseEnd,
// 资源加载时间
resourceLoad: navEntry.loadEventStart - navEntry.domContentLoadedEventEnd,
// 首屏时间
firstScreen: navEntry.domContentLoadedEventEnd - navEntry.startTime,
// 页面完全加载时间
load: navEntry.loadEventEnd - navEntry.startTime,
// 传输大小(Level 2 新增)
transferSize: navEntry.transferSize,
// 协议(Level 2 新增)
protocol: navEntry.nextHopProtocol,
}
this.report('performance', data)
}
reportNavigation(entry) {
const data = {
type: 'navigation',
url: entry.name,
duration: entry.duration,
transferSize: entry.transferSize,
domContentLoaded: entry.domContentLoadedEventEnd - entry.domContentLoadedEventStart,
loadComplete: entry.loadEventEnd - entry.loadEventStart,
}
this.report('navigation', data)
}
reportResource(entry) {
const data = {
type: 'resource',
name: entry.name,
duration: entry.duration,
size: entry.transferSize,
protocol: entry.nextHopProtocol,
}
this.report('resource', data)
}
observeWebVitals() {
// LCP
new PerformanceObserver((list) => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
this.report('lcp', {
value: lastEntry.renderTime || lastEntry.loadTime,
element: lastEntry.element,
})
}).observe({ entryTypes: ['largest-contentful-paint'] })
// INP(Interaction to Next Paint,2024年3月取代FID成为核心指标)
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.report('inp', {
value: entry.duration,
interactionType: entry.name, // pointerdown, keydown 等
})
}
}).observe({ type: 'event', buffered: true })
// 注意:FID 已被 INP 取代,如仍需监控 FID 可保留以下代码
// new PerformanceObserver((list) => {
// for (const entry of list.getEntries()) {
// this.report('fid', {
// value: entry.processingStart - entry.startTime,
// })
// }
// }).observe({ entryTypes: ['first-input'] })
// CLS
let clsScore = 0
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsScore += entry.value
}
}
this.report('cls', { value: clsScore })
}).observe({ entryTypes: ['layout-shift'] })
}
report(type, data) {
// 上报数据
console.log(`[${type}]`, data)
// 实际项目中应该发送到服务器
// navigator.sendBeacon('/api/monitor', JSON.stringify({ type, data }))
}
}
// 使用
const monitor = new PerformanceMonitor()2. 接口性能监控
javascript
class APIMonitor {
constructor() {
this.interceptFetch()
this.interceptXHR()
}
interceptFetch() {
const originalFetch = window.fetch
window.fetch = async (...args) => {
const startTime = Date.now()
const url = args[0]
try {
const response = await originalFetch(...args)
const duration = Date.now() - startTime
this.report({
type: 'fetch',
url,
method: args[1]?.method || 'GET',
status: response.status,
duration,
success: response.ok,
})
return response
} catch (error) {
const duration = Date.now() - startTime
this.report({
type: 'fetch',
url,
method: args[1]?.method || 'GET',
duration,
success: false,
error: error.message,
})
throw error
}
}
}
interceptXHR() {
const originalOpen = XMLHttpRequest.prototype.open
const originalSend = XMLHttpRequest.prototype.send
XMLHttpRequest.prototype.open = function(method, url) {
this._method = method
this._url = url
this._startTime = Date.now()
return originalOpen.apply(this, arguments)
}
XMLHttpRequest.prototype.send = function() {
this.addEventListener('loadend', () => {
const duration = Date.now() - this._startTime
this.report({
type: 'xhr',
url: this._url,
method: this._method,
status: this.status,
duration,
success: this.status >= 200 && this.status < 300,
})
})
return originalSend.apply(this, arguments)
}
}
report(data) {
console.log('[API]', data)
// 上报数据
}
}
// 使用
const apiMonitor = new APIMonitor()错误监控
1. JS 错误监控
javascript
class ErrorMonitor {
constructor() {
this.init()
}
init() {
// 监听全局错误
window.addEventListener('error', (event) => {
if (event.target !== window) {
// 资源加载错误
this.reportResourceError(event)
} else {
// JS 错误
this.reportJSError(event)
}
}, true)
// 监听 Promise 错误
window.addEventListener('unhandledrejection', (event) => {
this.reportPromiseError(event)
})
// 监听 Vue 错误
if (window.Vue) {
Vue.config.errorHandler = (err, vm, info) => {
this.reportVueError(err, vm, info)
}
}
// 监听 React 错误
// 使用 Error Boundary
}
reportJSError(event) {
const data = {
type: 'js-error',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
timestamp: Date.now(),
url: location.href,
userAgent: navigator.userAgent,
}
this.report(data)
}
reportResourceError(event) {
const data = {
type: 'resource-error',
tagName: event.target.tagName,
src: event.target.src || event.target.href,
timestamp: Date.now(),
url: location.href,
}
this.report(data)
}
reportPromiseError(event) {
const data = {
type: 'promise-error',
reason: event.reason,
promise: event.promise,
timestamp: Date.now(),
url: location.href,
}
this.report(data)
}
reportVueError(err, vm, info) {
const data = {
type: 'vue-error',
message: err.message,
stack: err.stack,
info,
componentName: vm.$options.name,
timestamp: Date.now(),
url: location.href,
}
this.report(data)
}
report(data) {
console.error('[Error]', data)
// 上报数据
navigator.sendBeacon('/api/error', JSON.stringify(data))
}
}
// 使用
const errorMonitor = new ErrorMonitor()2. React Error Boundary
javascript
class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error) {
return { hasError: true }
}
componentDidCatch(error, errorInfo) {
// 上报错误
this.reportError({
type: 'react-error',
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
timestamp: Date.now(),
url: location.href,
})
}
reportError(data) {
console.error('[React Error]', data)
navigator.sendBeacon('/api/error', JSON.stringify(data))
}
render() {
if (this.state.hasError) {
return <div>出错了</div>
}
return this.props.children
}
}
// 使用
<ErrorBoundary>
<App />
</ErrorBoundary>行为监控
1. 用户行为追踪
javascript
class BehaviorMonitor {
constructor() {
this.init()
}
init() {
// 监听页面访问
this.trackPageView()
// 监听点击事件
this.trackClick()
// 监听页面停留时间
this.trackStayTime()
// 监听页面可见性
this.trackVisibility()
}
trackPageView() {
const data = {
type: 'page-view',
url: location.href,
title: document.title,
referrer: document.referrer,
timestamp: Date.now(),
}
this.report(data)
}
trackClick() {
document.addEventListener('click', (event) => {
const target = event.target
const data = {
type: 'click',
tagName: target.tagName,
id: target.id,
className: target.className,
text: target.innerText?.slice(0, 50),
x: event.clientX,
y: event.clientY,
timestamp: Date.now(),
}
this.report(data)
}, true)
}
trackStayTime() {
let startTime = Date.now()
window.addEventListener('beforeunload', () => {
const stayTime = Date.now() - startTime
this.report({
type: 'stay-time',
url: location.href,
duration: stayTime,
timestamp: Date.now(),
})
})
}
trackVisibility() {
document.addEventListener('visibilitychange', () => {
const data = {
type: 'visibility',
visible: !document.hidden,
timestamp: Date.now(),
}
this.report(data)
})
}
report(data) {
console.log('[Behavior]', data)
// 上报数据
}
}
// 使用
const behaviorMonitor = new BehaviorMonitor()数据上报
1. 上报策略
javascript
class Reporter {
constructor(options = {}) {
this.url = options.url || '/api/monitor'
this.queue = []
this.maxSize = options.maxSize || 10
this.timeout = options.timeout || 5000
this.timer = null
}
// 添加数据到队列
add(data) {
this.queue.push({
...data,
timestamp: Date.now(),
url: location.href,
userAgent: navigator.userAgent,
})
// 队列满了立即上报
if (this.queue.length >= this.maxSize) {
this.flush()
} else {
// 否则延迟上报
this.scheduleFlush()
}
}
// 延迟上报
scheduleFlush() {
if (this.timer) {
clearTimeout(this.timer)
}
this.timer = setTimeout(() => {
this.flush()
}, this.timeout)
}
// 立即上报
flush() {
if (this.queue.length === 0) return
const data = this.queue.splice(0, this.maxSize)
// 使用 sendBeacon 上报(页面卸载时也能发送)
if (navigator.sendBeacon) {
navigator.sendBeacon(this.url, JSON.stringify(data))
} else {
// 降级使用 fetch
fetch(this.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
keepalive: true,
}).catch(console.error)
}
}
// 页面卸载时上报
beforeUnload() {
window.addEventListener('beforeunload', () => {
this.flush()
})
}
}
// 使用
const reporter = new Reporter({
url: '/api/monitor',
maxSize: 10,
timeout: 5000,
})
reporter.add({ type: 'performance', data: {} })2. 数据压缩
javascript
// 使用 pako 压缩数据
import pako from 'pako'
function compressData(data) {
const json = JSON.stringify(data)
const compressed = pako.gzip(json)
return compressed
}
// 上报压缩数据
fetch('/api/monitor', {
method: 'POST',
headers: {
'Content-Type': 'application/octet-stream',
'Content-Encoding': 'gzip',
},
body: compressData(data),
})实战案例
完整监控系统
javascript
class Monitor {
constructor(options = {}) {
this.options = options
this.reporter = new Reporter(options.reporter)
this.init()
}
init() {
// 性能监控
this.performanceMonitor = new PerformanceMonitor()
this.performanceMonitor.onReport = (data) => {
this.reporter.add(data)
}
// 错误监控
this.errorMonitor = new ErrorMonitor()
this.errorMonitor.onReport = (data) => {
this.reporter.add(data)
}
// 行为监控
this.behaviorMonitor = new BehaviorMonitor()
this.behaviorMonitor.onReport = (data) => {
this.reporter.add(data)
}
// 接口监控
this.apiMonitor = new APIMonitor()
this.apiMonitor.onReport = (data) => {
this.reporter.add(data)
}
// 页面卸载时上报
window.addEventListener('beforeunload', () => {
this.reporter.flush()
})
}
}
// 使用
const monitor = new Monitor({
reporter: {
url: '/api/monitor',
maxSize: 10,
timeout: 5000,
},
})最佳实践
- 采样上报:不是所有数据都需要上报,可以设置采样率
- 数据压缩:上报前压缩数据,减少网络传输
- 批量上报:积累一定数量后批量上报,减少请求次数
- 错误去重:相同错误只上报一次
- 用户标识:添加用户 ID,方便问题定位
- 环境信息:记录浏览器、操作系统等信息
- 隐私保护:不要上报敏感信息
最新知识补充(2024-2025)
1. INP 取代 FID 成为 Core Web Vitals 核心指标
2024 年 3 月,Google 正式将 INP (Interaction to Next Paint) 替代 FID (First Input Delay) 作为 Core Web Vitals 的三大核心指标之一(与 LCP、CLS 并列)。
为什么替换?
- FID 仅衡量首次输入的延迟,无法反映整个页面生命周期的交互响应性
- INP 衡量用户与页面的所有交互的延迟,更能代表真实用户体验
- INP 考虑了从交互开始到浏览器绘制下一帧的完整时间
INP 评分标准:
- 良好:<= 200ms
- 需要改进:200ms - 500ms
- 较差:> 500ms
使用 web-vitals 库监控 INP:
javascript
import { onINP } from 'web-vitals'
onINP((metric) => {
console.log('INP:', metric.value)
console.log('评级:', metric.rating) // 'good' | 'needs-improvement' | 'poor'
})2. Navigation Timing Level 2 API
performance.timing 已被标记为废弃(Deprecated),推荐使用 Navigation Timing Level 2 API:
javascript
// ❌ 旧写法(已废弃)
const timing = performance.timing
const dns = timing.domainLookupEnd - timing.domainLookupStart
// ✅ 新写法(Navigation Timing Level 2)
const [navEntry] = performance.getEntriesByType('navigation')
const dns = navEntry.domainLookupEnd - navEntry.domainLookupStart
const transferSize = navEntry.transferSize // Level 2 新增
const protocol = navEntry.nextHopProtocol // Level 2 新增(如 h2, h3)Level 2 新增属性:transferSize、encodedBodySize、decodedBodySize、nextHopProtocol、serverTiming 等。
3. web-vitals v4 库
web-vitals 库已更新到 v4 版本,主要变化:
- 新增
onINP()替代已废弃的onFID() - 支持 attribution 模式,提供更详细的性能归因信息
- 支持 Soft Navigation(SPA 路由切换的性能监控)
javascript
// web-vitals v4 + attribution
import { onINP, onLCP, onCLS } from 'web-vitals/attribution'
onINP((metric) => {
console.log('INP:', metric.value, metric.rating)
// attribution 提供详细信息
console.log('交互类型:', metric.attribution.interactionType)
console.log('目标元素:', metric.attribution.eventTarget)
})参考资料
💡 学习建议:前端监控是保障应用质量的重要手段。建议先学习性能监控和错误监控,然后通过实战项目积累经验。重点关注 Web Vitals、错误捕获、数据上报等核心知识点。