Skip to content

前端监控系统

⚠️ 本文档部分内容已过时,正在更新中。请参考最新官方文档获取最新信息。

更新时间: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,
  },
})

最佳实践

  1. 采样上报:不是所有数据都需要上报,可以设置采样率
  2. 数据压缩:上报前压缩数据,减少网络传输
  3. 批量上报:积累一定数量后批量上报,减少请求次数
  4. 错误去重:相同错误只上报一次
  5. 用户标识:添加用户 ID,方便问题定位
  6. 环境信息:记录浏览器、操作系统等信息
  7. 隐私保护:不要上报敏感信息

最新知识补充(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 新增属性transferSizeencodedBodySizedecodedBodySizenextHopProtocolserverTiming 等。

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、错误捕获、数据上报等核心知识点。

基于 VitePress 构建