性能异常监控

页面崩溃是"挂了",性能异常是"活着但很难用"。用户打开页面要等 3 秒、滚动掉帧、点击按钮没反应……这些问题不会触发错误上报,但对体验的伤害一点不小。

性能监控的核心思路是:持续采集关键指标,超过阈值就上报,把"用户觉得卡"变成可量化的数据

我用过四种手段,覆盖不同的性能问题:

手段监控什么原理
长任务检测主线程被阻塞超过 100ms 的任务PerformanceObserver 监听 longtask
内存监控JS 堆内存使用率performance.memory 定期采样
帧率监控每秒渲染帧数(FPS)requestAnimationFrame 计数
卡顿检测主线程完全无响应setInterval 心跳检测

监控长任务

长任务是页面卡顿的直接原因。当主线程被一个任务霸占超过 100ms,用户就会感觉到"卡"。浏览器提供了 PerformanceObserver API,可以直接监听这些长任务。

if ('PerformanceObserver' in window) {
  const observer = new PerformanceObserver((list) => {
    list.getEntries().forEach((entry) => {
      if (entry.duration > 100) {
        reportLongTask(entry)
      }
    })
  })

  observer.observe({ entryTypes: ['longtask'] })
}

光知道"有长任务"还不够,需要聚合分析才有价值:

let longTasks: Array<{ startTime: number; duration: number; name: string }> = []

function reportLongTask(entry: PerformanceEntry) {
  const now = performance.now()

  longTasks.push({
    startTime: entry.startTime,
    duration: entry.duration,
    name: entry.name,
  })

  // 如果最近 5 秒内累计阻塞超过 1 秒,说明页面持续卡顿
  const recentTasks = longTasks.filter((task) => now - task.startTime < 5000)
  const totalBlocking = recentTasks.reduce((sum, t) => sum + t.duration, 0)

  if (totalBlocking > 1000) {
    report({
      type: 'frequent_long_tasks',
      totalBlocking,
      averageDuration: totalBlocking / recentTasks.length,
      tasks: recentTasks,
    })
  }

  // 只保留最近 30 秒的任务,避免内存无限增长
  longTasks = longTasks.filter((task) => now - task.startTime < 30000)
}

局限性longtask 只在 Chrome/Edge 中支持,Safari 和 Firefox 不支持。

监控内存

内存泄漏不会立刻崩溃,但会让页面越来越慢,最终被浏览器杀掉。定期采样 JS 堆内存使用率,可以提前发现内存问题。

if ('memory' in performance) {
  setInterval(() => {
    const memory = (performance as any).memory
    const used = memory.usedJSHeapSize
    const total = memory.totalJSHeapSize
    const usage = used / total

    if (usage > 0.9) {
      report({
        type: 'high_memory_usage',
        usage: `${(usage * 100).toFixed(1)}%`,
        used: formatBytes(used),
        total: formatBytes(total),
      })
    }
  }, 10000)
}

局限性performance.memory 是 Chrome 特有的非标准 API,其他浏览器不支持。目前没有跨浏览器的 JS 堆内存监控方案,这是浏览器厂商的选择——他们认为暴露精确内存信息存在安全和隐私风险。

监控帧率

帧率是用户感知流畅度的直接指标。60 FPS 是理想值,低于 30 FPS 就会明显感觉卡顿。用 requestAnimationFrame 可以精确计算每秒渲染了多少帧。

let lastTime = performance.now()
let frameCount = 0

const checkFPS = () => {
  frameCount++
  const currentTime = performance.now()
  const elapsed = currentTime - lastTime

  if (elapsed >= 1000) {
    const fps = Math.round((frameCount * 1000) / elapsed)

    if (fps < 30) {
      report({
        type: 'low_fps',
        fps,
        timestamp: Date.now(),
      })
    }

    frameCount = 0
    lastTime = currentTime
  }

  requestAnimationFrame(checkFPS)
}

checkFPS()

一个细节:FPS 监控本身会消耗性能(每帧都在计算),但在实际测试中影响可以忽略不计。如果担心,可以降低采样频率,比如每 5 帧计算一次。

监控卡顿

前面的长任务检测依赖 PerformanceObserver,但有些浏览器不支持。卡顿检测是一个更通用的兜底方案:用 setInterval 做心跳,如果回调延迟超过预期,说明主线程被阻塞了。

let lastCheck = Date.now()

setInterval(() => {
  const now = Date.now()
  const timeDiff = now - lastCheck

  // setInterval 应该每秒触发 1 次
  // 如果实际间隔超过 2 秒,说明主线程被卡住了
  if (timeDiff > 2000) {
    report({
      type: 'main_thread_stuck',
      stuckTime: timeDiff,
    })
  }

  lastCheck = now
}, 1000)

原理setInterval 的回调需要等主线程空闲才能执行。如果主线程被一个长任务霸占,回调就会被延迟。通过检测这个延迟,就能间接判断主线程是否卡住了。

四种手段怎么选

场景推荐手段
精确定位哪个任务导致卡顿长任务检测
发现内存泄漏内存监控
衡量整体流畅度帧率监控
浏览器不支持 PerformanceObserver 时的兜底卡顿检测

实际项目中,长任务检测 + 帧率监控是最常见的组合,前者告诉你"哪里卡",后者告诉你"有多卡"。内存监控适合长时间运行的 SPA,卡顿检测则是兼容性最好的兜底方案。