返回笔记首页

长时间运行内存泄漏问题完整技术文档

主题配置

目录

  1. 技术实现方案
  2. 可运行代码Demo
  3. 简历描述模板
  4. 面试SOP标准回答
  5. 难点与亮点分析
  6. 真实项目经验表达

技术实现方案

4.1 常见内存泄漏场景

场景1: 定时器未清除

javascript
// ❌ 错误示范
mounted() {
  this.timer = setInterval(() => {
    this.updateData()
  }, 1000)
}
// 组件销毁时定时器仍在运行

// ✅ 正确做法
onMounted(() => {
  timer = setInterval(() => updateData(), 1000)
})
onUnmounted(() => {
  clearInterval(timer)
})
场景2: 事件监听器未移除
javascript
// ❌ 错误示范
mounted() {
  window.addEventListener('resize', this.handleResize)
}
// 组件销毁后事件仍然绑定

// ✅ 正确做法
onMounted(() => {
  window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
})
场景3: 闭包引用
javascript
// ❌ 错误示范
function createClosure() {
  const largeData = new Array(1000000).fill('data')
  return function() {
    console.log(largeData[0])
  }
}
// largeData无法被垃圾回收

// ✅ 正确做法
function createClosure() {
  const largeData = new Array(1000000).fill('data')
  const firstItem = largeData[0]
  return function() {
    console.log(firstItem)
  }
  // largeData可以被回收
}
场景4: DOM引用未释放
javascript
// ❌ 错误示范
const element = document.getElementById('myDiv')
document.body.removeChild(element)
// element仍然持有DOM引用

// ✅ 正确做法
let element = document.getElementById('myDiv')
document.body.removeChild(element)
element = null // 释放引用
场景5: 全局变量累积
javascript
// ❌ 错误示范
window.dataCache = []
function addData(data) {
  window.dataCache.push(data)
}
// 数据不断累积

// ✅ 正确做法
const MAX_CACHE_SIZE = 1000
function addData(data) {
  dataCache.push(data)
  if (dataCache.length > MAX_CACHE_SIZE) {
    dataCache.shift() // 移除最旧数据
  }
}
场景6: Canvas未销毁
javascript
// ❌ 错误示范
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
// Canvas对象占用大量内存

// ✅ 正确做法
onUnmounted(() => {
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  canvas.width = 0
  canvas.height = 0
  canvas = null
  ctx = null
})
场景7: WebSocket连接未关闭
javascript
// ❌ 错误示范
const ws = new WebSocket('ws://example.com')
// 组件销毁但连接仍在

// ✅ 正确做法
onUnmounted(() => {
  if (ws) {
    ws.close()
    ws = null
  }
})

4.2 内存泄漏排查工具

Chrome DevTools Memory面板

  1. Heap Snapshot(堆快照)
    • 作用: 捕获某一时刻的内存状态
    • 使用: Take snapshot → 对比多个快照
    • 分析: 找出持续增长的对象
    • 指标: Shallow Size(对象自身大小)、Retained Size(对象+引用大小)
  2. Allocation Timeline(分配时间线)
    • 作用: 记录内存分配过程
    • 使用: Start recording → 操作页面 → Stop
    • 分析: 找出频繁分配的对象
    • 定位: 点击蓝条查看调用栈
  3. Allocation Instrumentation(分配仪表)
    • 作用: 实时记录对象分配
    • 使用: 选择Allocation instrumentation on timeline
    • 特点: 性能开销较大,但信息详细
  4. Performance Monitor(性能监控)
    • 作用: 实时监控内存使用
    • 使用: Cmd+Shift+P → Show Performance Monitor
    • 指标: JS heap size、Nodes、Listeners、Documents
排查流程
plain
1. 打开DevTools Memory面板
2. 记录基线快照(Snapshot 1)
3. 执行可能泄漏的操作
4. 记录第二个快照(Snapshot 2)
5. 重复操作多次
6. 记录第三个快照(Snapshot 3)
7. 对比快照,找出持续增长的对象
8. 查看对象的Retainers(引用链)
9. 定位代码位置
10. 修复泄漏

4.3 防止内存泄漏策略

组件生命周期管理

javascript
// Vue3 Composition API模式
export default {
  setup() {
    const timers = []
    const listeners = []
    const resources = []

    // 注册定时器
    const registerTimer = (timer) => {
      timers.push(timer)
    }

    // 注册监听器
    const registerListener = (target, event, handler) => {
      target.addEventListener(event, handler)
      listeners.push({ target, event, handler })
    }

    // 注册资源
    const registerResource = (resource) => {
      resources.push(resource)
    }

    // 统一清理
    onUnmounted(() => {
      // 清除定时器
      timers.forEach(timer => clearInterval(timer))

      // 移除监听器
      listeners.forEach(({ target, event, handler }) => {
        target.removeEventListener(event, handler)
      })

      // 释放资源
      resources.forEach(resource => {
        if (resource.dispose) resource.dispose()
        if (resource.destroy) resource.destroy()
      })

      // 清空数组
      timers.length = 0
      listeners.length = 0
      resources.length = 0
    })

    return {
      registerTimer,
      registerListener,
      registerResource
    }
  }
}
图表实例销毁(ECharts)
javascript
onUnmounted(() => {
  if (chartInstance) {
    chartInstance.dispose() // 销毁实例
    chartInstance = null
  }
})
定时任务优化
javascript
// 任务调度管理器
class TaskScheduler {
  constructor() {
    this.tasks = new Map()
    this.paused = false
  }

  // 添加任务
  addTask(name, fn, interval) {
    const timer = setInterval(() => {
      if (!this.paused) {
        fn()
      }
    }, interval)

    this.tasks.set(name, { fn, interval, timer })
  }

  // 移除任务
  removeTask(name) {
    const task = this.tasks.get(name)
    if (task) {
      clearInterval(task.timer)
      this.tasks.delete(name)
    }
  }

  // 暂停所有任务
  pauseAll() {
    this.paused = true
  }

  // 恢复所有任务
  resumeAll() {
    this.paused = false
  }

  // 清空所有任务
  clear() {
    this.tasks.forEach(task => clearInterval(task.timer))
    this.tasks.clear()
  }
}
RequestIdleCallback空闲执行
javascript
function scheduleIdleTask(task) {
  if ('requestIdleCallback' in window) {
    requestIdleCallback((deadline) => {
      // 在空闲时间执行
      if (deadline.timeRemaining() > 0) {
        task()
      }
    })
  } else {
    // 降级方案
    setTimeout(task, 1)
  }
}

4.4 内存监控与告警

Performance Observer监控

javascript
// 监控内存使用
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.entryType === 'measure') {
      console.log('Memory:', entry.duration)
    }
  }
})

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

// 定期测量
setInterval(() => {
  performance.measure('memory-check')

  if (performance.memory) {
    const { usedJSHeapSize, totalJSHeapSize } = performance.memory
    const usage = (usedJSHeapSize / totalJSHeapSize) * 100

    if (usage > 90) {
      console.warn('内存使用率过高:', usage.toFixed(2) + '%')
    }
  }
}, 10000)
内存阈值告警
javascript
class MemoryMonitor {
  constructor(options = {}) {
    this.threshold = options.threshold || 0.9 // 90%
    this.checkInterval = options.checkInterval || 10000
    this.onWarning = options.onWarning
    this.onError = options.onError
    this.timer = null
  }

  start() {
    this.timer = setInterval(() => {
      this.check()
    }, this.checkInterval)
  }

  stop() {
    if (this.timer) {
      clearInterval(this.timer)
      this.timer = null
    }
  }

  check() {
    if (!performance.memory) return

    const { usedJSHeapSize, jsHeapSizeLimit } = performance.memory
    const usage = usedJSHeapSize / jsHeapSizeLimit

    if (usage > this.threshold) {
      const info = {
        usage: (usage * 100).toFixed(2) + '%',
        used: (usedJSHeapSize / 1024 / 1024).toFixed(2) + 'MB',
        limit: (jsHeapSizeLimit / 1024 / 1024).toFixed(2) + 'MB'
      }

      if (usage > 0.95) {
        // 严重告警
        if (this.onError) {
          this.onError(info)
        }
      } else {
        // 警告
        if (this.onWarning) {
          this.onWarning(info)
        }
      }
    }
  }
}
自动刷新机制
javascript
// 达到阈值自动刷新
const autoRefreshManager = {
  threshold: 0.9,
  checkInterval: 60000, // 1分钟检查一次
  maxRuntime: 24 * 60 * 60 * 1000, // 24小时
  startTime: Date.now(),

  init() {
    setInterval(() => {
      this.checkMemory()
      this.checkRuntime()
    }, this.checkInterval)
  },

  checkMemory() {
    if (!performance.memory) return

    const { usedJSHeapSize, jsHeapSizeLimit } = performance.memory
    const usage = usedJSHeapSize / jsHeapSizeLimit

    if (usage > this.threshold) {
      console.warn('内存使用过高,准备刷新页面')
      this.refresh()
    }
  },

  checkRuntime() {
    const runtime = Date.now() - this.startTime
    if (runtime > this.maxRuntime) {
      console.log('运行时间过长,准备刷新页面')
      this.refresh()
    }
  },

  refresh() {
    // 保存必要数据到sessionStorage
    sessionStorage.setItem('autoRefresh', 'true')

    // 延迟刷新,给用户提示
    setTimeout(() => {
      location.reload()
    }, 3000)
  }
}

4.5 长时间运行稳定性方案

心跳健康检查

javascript
class HealthChecker {
  constructor(options = {}) {
    this.interval = options.interval || 30000
    this.timeout = options.timeout || 5000
    this.maxFailures = options.maxFailures || 3
    this.failures = 0
    this.timer = null
    this.onHealthy = options.onHealthy
    this.onUnhealthy = options.onUnhealthy
  }

  start() {
    this.timer = setInterval(() => {
      this.check()
    }, this.interval)
  }

  stop() {
    clearInterval(this.timer)
  }

  async check() {
    try {
      const startTime = Date.now()

      // 检查网络连通性
      const response = await fetch('/api/health', {
        method: 'GET',
        timeout: this.timeout
      })

      const elapsed = Date.now() - startTime

      if (response.ok && elapsed < this.timeout) {
        this.failures = 0
        if (this.onHealthy) {
          this.onHealthy({ elapsed })
        }
      } else {
        this.handleFailure()
      }
    } catch (error) {
      this.handleFailure(error)
    }
  }

  handleFailure(error) {
    this.failures++

    if (this.failures >= this.maxFailures) {
      if (this.onUnhealthy) {
        this.onUnhealthy({
          failures: this.failures,
          error: error?.message
        })
      }
    }
  }
}
定期页面刷新(24小时)
javascript
// 设置24小时后刷新
const autoRefreshIn24Hours = () => {
  const hours24 = 24 * 60 * 60 * 1000

  setTimeout(() => {
    console.log('24小时自动刷新')

    // 保存状态
    const state = {
      timestamp: Date.now(),
      reason: 'scheduled_refresh'
    }
    sessionStorage.setItem('refreshState', JSON.stringify(state))

    // 刷新页面
    location.reload()
  }, hours24)
}

// 页面加载时检查是否是自动刷新
onMounted(() => {
  const refreshState = sessionStorage.getItem('refreshState')
  if (refreshState) {
    const state = JSON.parse(refreshState)
    console.log('从自动刷新恢复:', state)
    sessionStorage.removeItem('refreshState')
  }

  autoRefreshIn24Hours()
})
异常捕获与降级
javascript
// 全局错误处理
window.addEventListener('error', (event) => {
  console.error('全局错误:', event.error)

  // 记录错误
  logError({
    type: 'error',
    message: event.error?.message,
    stack: event.error?.stack,
    filename: event.filename,
    lineno: event.lineno,
    colno: event.colno
  })
})

// Promise未捕获错误
window.addEventListener('unhandledrejection', (event) => {
  console.error('未捕获的Promise错误:', event.reason)

  logError({
    type: 'unhandledRejection',
    message: event.reason?.message,
    stack: event.reason?.stack
  })
})

// Vue错误处理
app.config.errorHandler = (err, vm, info) => {
  console.error('Vue错误:', err, info)

  logError({
    type: 'vue',
    message: err.message,
    stack: err.stack,
    info: info
  })
}

可运行代码Demo

Demo 1: 资源管理器组件

vue
<template>
  <div class="resource-manager">
    <div class="status-panel">
      <div class="status-item">
        <span class="label">定时器:</span>
        <span class="value">{{ timerCount }}</span>
      </div>
      <div class="status-item">
        <span class="label">监听器:</span>
        <span class="value">{{ listenerCount }}</span>
      </div>
      <div class="status-item">
        <span class="label">资源:</span>
        <span class="value">{{ resourceCount }}</span>
      </div>
    </div>

    <div class="demo-area">
      <h3>内存泄漏演示</h3>
      <div class="demo-buttons">
        <button @click="addLeakyTimer">添加泄漏定时器</button>
        <button @click="addSafeTimer">添加安全定时器</button>
        <button @click="addLeakyListener">添加泄漏监听器</button>
        <button @click="addSafeListener">添加安全监听器</button>
        <button @click="clearAll">清理所有资源</button>
      </div>

      <div class="log-area">
        <div
          class="log-item"
          v-for="(log, index) in logs"
          :key="index"
        >
          {{ log }}
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onUnmounted } from 'vue'

// 资源管理器类
class ResourceManager {
  constructor() {
    this.timers = []
    this.listeners = []
    this.resources = []
  }

  // 注册定时器
  registerTimer(timer) {
    this.timers.push(timer)
    return timer
  }

  // 注册监听器
  registerListener(target, event, handler) {
    target.addEventListener(event, handler)
    this.listeners.push({ target, event, handler })
  }

  // 注册资源
  registerResource(resource) {
    this.resources.push(resource)
  }

  // 清理所有资源
  cleanup() {
    // 清除定时器
    this.timers.forEach(timer => {
      clearInterval(timer)
      clearTimeout(timer)
    })
    console.log(`清除了 ${this.timers.length} 个定时器`)

    // 移除监听器
    this.listeners.forEach(({ target, event, handler }) => {
      target.removeEventListener(event, handler)
    })
    console.log(`移除了 ${this.listeners.length} 个监听器`)

    // 释放资源
    this.resources.forEach(resource => {
      if (resource.dispose) resource.dispose()
      if (resource.destroy) resource.destroy()
    })
    console.log(`释放了 ${this.resources.length} 个资源`)

    // 清空数组
    this.timers = []
    this.listeners = []
    this.resources = []
  }

  // 获取统计
  getStats() {
    return {
      timers: this.timers.length,
      listeners: this.listeners.length,
      resources: this.resources.length
    }
  }
}

// 组件状态
const manager = new ResourceManager()
const timerCount = ref(0)
const listenerCount = ref(0)
const resourceCount = ref(0)
const logs = ref([])
const leakyTimers = [] // 模拟泄漏的定时器

// 添加日志
const addLog = (message) => {
  const timestamp = new Date().toLocaleTimeString()
  logs.value.unshift(`[${timestamp}] ${message}`)
  if (logs.value.length > 10) {
    logs.value.pop()
  }
}

// 更新统计
const updateStats = () => {
  const stats = manager.getStats()
  timerCount.value = stats.timers
  listenerCount.value = stats.listeners
  resourceCount.value = stats.resources
}

// 添加泄漏的定时器(不清理)
const addLeakyTimer = () => {
  const timer = setInterval(() => {
    console.log('泄漏的定时器仍在运行')
  }, 1000)

  leakyTimers.push(timer)
  addLog('❌ 添加了泄漏定时器(未注册管理)')
}

// 添加安全的定时器(会清理)
const addSafeTimer = () => {
  const timer = setInterval(() => {
    console.log('安全的定时器运行中')
  }, 1000)

  manager.registerTimer(timer)
  updateStats()
  addLog('✅ 添加了安全定时器(已注册管理)')
}

// 添加泄漏的监听器(不清理)
const addLeakyListener = () => {
  const handler = () => {
    console.log('泄漏的监听器触发')
  }

  window.addEventListener('resize', handler)
  addLog('❌ 添加了泄漏监听器(未注册管理)')
}

// 添加安全的监听器(会清理)
const addSafeListener = () => {
  const handler = () => {
    console.log('安全的监听器触发')
  }

  manager.registerListener(window, 'resize', handler)
  updateStats()
  addLog('✅ 添加了安全监听器(已注册管理)')
}

// 清理所有资源
const clearAll = () => {
  manager.cleanup()
  updateStats()
  addLog('🧹 清理了所有管理的资源')
}

// 组件卸载时清理
onUnmounted(() => {
  clearAll()
  addLog('组件销毁,执行清理')
})
</script>

<style scoped>
.resource-manager {
  padding: 20px;
  background: #0a0e27;
  border-radius: 10px;
  color: #fff;
}

.status-panel {
  display: flex;
  gap: 20px;
  padding: 20px;
  background: rgba(0, 246, 255, 0.1);
  border-radius: 8px;
  margin-bottom: 20px;
}

.status-item {
  flex: 1;
  text-align: center;
}

.label {
  display: block;
  color: #aaa;
  font-size: 14px;
  margin-bottom: 8px;
}

.value {
  display: block;
  color: #00f6ff;
  font-size: 32px;
  font-weight: bold;
}

.demo-area h3 {
  color: #00f6ff;
  margin-bottom: 15px;
}

.demo-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 20px;
}

.demo-buttons button {
  padding: 10px 20px;
  background: #00f6ff;
  border: none;
  border-radius: 5px;
  color: #000;
  font-weight: bold;
  cursor: pointer;
  transition: all 0.3s ease;
}

.demo-buttons button:hover {
  background: #00d9e6;
  transform: translateY(-2px);
}

.log-area {
  background: rgba(0, 0, 0, 0.3);
  border-radius: 5px;
  padding: 15px;
  max-height: 300px;
  overflow-y: auto;
  font-family: 'Courier New', monospace;
  font-size: 14px;
}

.log-item {
  padding: 5px 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  color: #ccc;
}

.log-item:last-child {
  border-bottom: none;
}
</style>

Demo 2: 内存监控组件

vue
<template>
  <div class="memory-monitor">
    <div class="monitor-header">
      <h3>内存监控面板</h3>
      <div class="controls">
        <button @click="startMonitor">开始监控</button>
        <button @click="stopMonitor">停止监控</button>
        <button @click="takeSnapshot">拍摄快照</button>
        <button @click="forceGC" v-if="canForceGC">强制GC</button>
      </div>
    </div>

    <div class="memory-stats">
      <div class="stat-card">
        <div class="stat-label">已用内存</div>
        <div class="stat-value" :class="usageClass">
          {{ usedMemory }}
        </div>
        <div class="stat-sub">/ {{ totalMemory }}</div>
      </div>

      <div class="stat-card">
        <div class="stat-label">使用率</div>
        <div class="stat-value" :class="usageClass">
          {{ memoryUsage }}
        </div>
        <div class="stat-sub">
          <div class="usage-bar">
            <div
              class="usage-fill"
              :style="{ width: memoryUsage }"
              :class="usageClass"
            ></div>
          </div>
        </div>
      </div>

      <div class="stat-card">
        <div class="stat-label">监控时长</div>
        <div class="stat-value">{{ monitorDuration }}</div>
        <div class="stat-sub">秒</div>
      </div>

      <div class="stat-card">
        <div class="stat-label">告警次数</div>
        <div class="stat-value warning">{{ warningCount }}</div>
        <div class="stat-sub">次</div>
      </div>
    </div>

    <div class="memory-chart">
      <canvas ref="chartCanvas"></canvas>
    </div>

    <div class="snapshots" v-if="snapshots.length > 0">
      <h4>内存快照</h4>
      <div class="snapshot-list">
        <div
          class="snapshot-item"
          v-for="(snapshot, index) in snapshots"
          :key="index"
        >
          <span class="snapshot-time">{{ snapshot.time }}</span>
          <span class="snapshot-memory">{{ snapshot.memory }}</span>
          <span class="snapshot-usage">{{ snapshot.usage }}</span>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'

// 内存监控器类
class MemoryMonitor {
  constructor(options = {}) {
    this.interval = options.interval || 1000
    this.maxDataPoints = options.maxDataPoints || 60
    this.threshold = options.threshold || 0.9
    this.dataPoints = []
    this.timer = null
    this.startTime = null
    this.warningCount = 0
    this.onUpdate = options.onUpdate
    this.onWarning = options.onWarning
  }

  start() {
    if (this.timer) return

    this.startTime = Date.now()
    this.timer = setInterval(() => {
      this.collect()
    }, this.interval)
  }

  stop() {
    if (this.timer) {
      clearInterval(this.timer)
      this.timer = null
    }
  }

  collect() {
    if (!performance.memory) {
      console.warn('浏览器不支持performance.memory')
      return
    }

    const { usedJSHeapSize, totalJSHeapSize, jsHeapSizeLimit } = performance.memory

    const dataPoint = {
      timestamp: Date.now(),
      used: usedJSHeapSize,
      total: totalJSHeapSize,
      limit: jsHeapSizeLimit,
      usage: usedJSHeapSize / jsHeapSizeLimit
    }

    this.dataPoints.push(dataPoint)

    // 保持数据点数量
    if (this.dataPoints.length > this.maxDataPoints) {
      this.dataPoints.shift()
    }

    // 检查告警
    if (dataPoint.usage > this.threshold) {
      this.warningCount++
      if (this.onWarning) {
        this.onWarning(dataPoint)
      }
    }

    if (this.onUpdate) {
      this.onUpdate(dataPoint)
    }
  }

  getStats() {
    if (this.dataPoints.length === 0) {
      return null
    }

    const latest = this.dataPoints[this.dataPoints.length - 1]
    const duration = this.startTime ? Math.floor((Date.now() - this.startTime) / 1000) : 0

    return {
      used: latest.used,
      total: latest.total,
      usage: latest.usage,
      duration,
      warningCount: this.warningCount,
      dataPoints: this.dataPoints
    }
  }

  takeSnapshot() {
    const stats = this.getStats()
    if (!stats) return null

    return {
      time: new Date().toLocaleTimeString(),
      memory: (stats.used / 1024 / 1024).toFixed(2) + 'MB',
      usage: (stats.usage * 100).toFixed(2) + '%',
      raw: stats
    }
  }
}

// 组件状态
const monitor = new MemoryMonitor({
  interval: 1000,
  maxDataPoints: 60,
  threshold: 0.8,
  onUpdate: updateChart,
  onWarning: handleWarning
})

const usedMemory = ref('0MB')
const totalMemory = ref('0MB')
const memoryUsage = ref('0%')
const monitorDuration = ref(0)
const warningCount = ref(0)
const snapshots = ref([])
const chartCanvas = ref(null)
const canForceGC = ref('gc' in window && typeof window.gc === 'function')

let chartCtx = null
const chartData = []

// 使用率样式
const usageClass = computed(() => {
  const usage = parseFloat(memoryUsage.value)
  if (usage >= 90) return 'danger'
  if (usage >= 70) return 'warning'
  return 'normal'
})

// 更新图表
function updateChart(dataPoint) {
  usedMemory.value = (dataPoint.used / 1024 / 1024).toFixed(2) + 'MB'
  totalMemory.value = (dataPoint.total / 1024 / 1024).toFixed(2) + 'MB'
  memoryUsage.value = (dataPoint.usage * 100).toFixed(2) + '%'

  const stats = monitor.getStats()
  if (stats) {
    monitorDuration.value = stats.duration
    warningCount.value = stats.warningCount
  }

  // 更新图表数据
  chartData.push(dataPoint.usage * 100)
  if (chartData.length > 60) {
    chartData.shift()
  }

  drawChart()
}

// 处理告警
function handleWarning(dataPoint) {
  console.warn('内存使用过高:', dataPoint)
}

// 绘制图表
function drawChart() {
  if (!chartCtx) return

  const canvas = chartCanvas.value
  const width = canvas.width
  const height = canvas.height

  // 清空画布
  chartCtx.clearRect(0, 0, width, height)

  // 绘制网格
  chartCtx.strokeStyle = 'rgba(0, 246, 255, 0.1)'
  chartCtx.lineWidth = 1

  // 水平网格线
  for (let i = 0; i <= 10; i++) {
    const y = (height / 10) * i
    chartCtx.beginPath()
    chartCtx.moveTo(0, y)
    chartCtx.lineTo(width, y)
    chartCtx.stroke()
  }

  // 绘制数据线
  if (chartData.length < 2) return

  chartCtx.strokeStyle = '#00f6ff'
  chartCtx.lineWidth = 2
  chartCtx.beginPath()

  const step = width / (chartData.length - 1)

  chartData.forEach((value, index) => {
    const x = index * step
    const y = height - (value / 100) * height

    if (index === 0) {
      chartCtx.moveTo(x, y)
    } else {
      chartCtx.lineTo(x, y)
    }
  })

  chartCtx.stroke()

  // 填充渐变
  chartCtx.lineTo(width, height)
  chartCtx.lineTo(0, height)
  chartCtx.closePath()

  const gradient = chartCtx.createLinearGradient(0, 0, 0, height)
  gradient.addColorStop(0, 'rgba(0, 246, 255, 0.3)')
  gradient.addColorStop(1, 'rgba(0, 246, 255, 0)')

  chartCtx.fillStyle = gradient
  chartCtx.fill()
}

// 开始监控
const startMonitor = () => {
  monitor.start()
}

// 停止监控
const stopMonitor = () => {
  monitor.stop()
}

// 拍摄快照
const takeSnapshot = () => {
  const snapshot = monitor.takeSnapshot()
  if (snapshot) {
    snapshots.value.unshift(snapshot)
    if (snapshots.value.length > 10) {
      snapshots.value.pop()
    }
  }
}

// 强制垃圾回收
const forceGC = () => {
  if (window.gc) {
    window.gc()
    console.log('已触发垃圾回收')
  }
}

onMounted(() => {
  // 初始化Canvas
  const canvas = chartCanvas.value
  canvas.width = canvas.offsetWidth
  canvas.height = canvas.offsetHeight
  chartCtx = canvas.getContext('2d')

  // 自动开始监控
  startMonitor()
})

onUnmounted(() => {
  stopMonitor()
})
</script>

<style scoped>
.memory-monitor {
  padding: 20px;
  background: #0a0e27;
  border-radius: 10px;
  color: #fff;
}

.monitor-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}

.monitor-header h3 {
  color: #00f6ff;
  margin: 0;
}

.controls {
  display: flex;
  gap: 10px;
}

.controls button {
  padding: 8px 16px;
  background: #00f6ff;
  border: none;
  border-radius: 5px;
  color: #000;
  font-weight: bold;
  cursor: pointer;
  transition: all 0.3s ease;
}

.controls button:hover {
  background: #00d9e6;
  transform: translateY(-2px);
}

.memory-stats {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 15px;
  margin-bottom: 20px;
}

.stat-card {
  background: rgba(0, 246, 255, 0.1);
  border: 2px solid #00f6ff;
  border-radius: 8px;
  padding: 15px;
  text-align: center;
}

.stat-label {
  color: #aaa;
  font-size: 12px;
  margin-bottom: 8px;
}

.stat-value {
  font-size: 24px;
  font-weight: bold;
  margin-bottom: 5px;
}

.stat-value.normal {
  color: #4caf50;
}

.stat-value.warning {
  color: #ff9800;
}

.stat-value.danger {
  color: #f44336;
}

.stat-sub {
  color: #666;
  font-size: 12px;
}

.usage-bar {
  width: 100%;
  height: 4px;
  background: rgba(255, 255, 255, 0.1);
  border-radius: 2px;
  overflow: hidden;
  margin-top: 5px;
}

.usage-fill {
  height: 100%;
  transition: width 0.3s ease;
}

.usage-fill.normal {
  background: #4caf50;
}

.usage-fill.warning {
  background: #ff9800;
}

.usage-fill.danger {
  background: #f44336;
}

.memory-chart {
  background: rgba(0, 0, 0, 0.3);
  border-radius: 8px;
  padding: 15px;
  margin-bottom: 20px;
}

.memory-chart canvas {
  width: 100%;
  height: 200px;
  display: block;
}

.snapshots h4 {
  color: #00f6ff;
  margin-bottom: 10px;
}

.snapshot-list {
  background: rgba(0, 0, 0, 0.3);
  border-radius: 5px;
  padding: 10px;
  max-height: 200px;
  overflow-y: auto;
}

.snapshot-item {
  display: flex;
  justify-content: space-between;
  padding: 8px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  font-size: 14px;
}

.snapshot-item:last-child {
  border-bottom: none;
}

.snapshot-time {
  color: #aaa;
}

.snapshot-memory {
  color: #00f6ff;
  font-weight: bold;
}

.snapshot-usage {
  color: #ff9800;
}
</style>

Demo 3: ECharts图表生命周期管理

vue
<template>
  <div class="chart-lifecycle-demo">
    <div class="controls">
      <button @click="createChart">创建图表</button>
      <button @click="updateChart">更新数据</button>
      <button @click="destroyChart">销毁图表</button>
      <button @click="recreateChart">重建图表</button>
    </div>

    <div class="chart-info">
      <span>图表状态: <strong :class="statusClass">{{ chartStatus }}</strong></span>
      <span>更新次数: {{ updateCount }}</span>
      <span>内存估算: {{ memoryEstimate }}</span>
    </div>

    <div ref="chartContainer" class="chart-container"></div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'

const chartContainer = ref(null)
let chartInstance = null
const chartStatus = ref('未创建')
const updateCount = ref(0)

// 状态样式
const statusClass = computed(() => {
  return {
    'status-active': chartStatus.value === '运行中',
    'status-inactive': chartStatus.value === '未创建',
    'status-destroyed': chartStatus.value === '已销毁'
  }
})

// 内存估算
const memoryEstimate = computed(() => {
  if (!chartInstance) return '0MB'
  // 粗略估算: 基础50KB + 每个数据点1KB
  const baseSize = 50
  const dataSize = updateCount.value * 1
  return ((baseSize + dataSize) / 1024).toFixed(2) + 'MB'
})

// 创建图表
const createChart = () => {
  if (chartInstance) {
    console.warn('图表已存在')
    return
  }

  chartInstance = echarts.init(chartContainer.value)

  const option = {
    title: {
      text: 'ECharts生命周期演示',
      textStyle: { color: '#00f6ff' }
    },
    tooltip: {
      trigger: 'axis'
    },
    xAxis: {
      type: 'category',
      data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
      axisLine: { lineStyle: { color: '#00f6ff' } }
    },
    yAxis: {
      type: 'value',
      axisLine: { lineStyle: { color: '#00f6ff' } }
    },
    series: [{
      data: [120, 200, 150, 80, 70, 110, 130],
      type: 'line',
      smooth: true,
      lineStyle: { color: '#00f6ff' },
      areaStyle: {
        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
          { offset: 0, color: 'rgba(0, 246, 255, 0.3)' },
          { offset: 1, color: 'rgba(0, 246, 255, 0)' }
        ])
      }
    }]
  }

  chartInstance.setOption(option)
  chartStatus.value = '运行中'

  console.log('图表已创建')
}

// 更新图表
const updateChart = () => {
  if (!chartInstance) {
    console.warn('请先创建图表')
    return
  }

  // 生成随机数据
  const data = Array.from({ length: 7 }, () =>
    Math.floor(Math.random() * 200) + 50
  )

  chartInstance.setOption({
    series: [{ data }]
  })

  updateCount.value++
  console.log('图表已更新')
}

// 销毁图表
const destroyChart = () => {
  if (!chartInstance) {
    console.warn('图表不存在')
    return
  }

  // 正确的销毁方法
  chartInstance.dispose()
  chartInstance = null
  chartStatus.value = '已销毁'

  console.log('图表已销毁,内存已释放')
}

// 重建图表
const recreateChart = () => {
  destroyChart()
  setTimeout(() => {
    createChart()
  }, 100)
}

onMounted(() => {
  createChart()
})

onUnmounted(() => {
  // 关键: 组件卸载时必须销毁图表实例
  destroyChart()
})
</script>

<style scoped>
.chart-lifecycle-demo {
  padding: 20px;
  background: #0a0e27;
  border-radius: 10px;
  color: #fff;
}

.controls {
  display: flex;
  gap: 10px;
  margin-bottom: 15px;
}

.controls button {
  padding: 10px 20px;
  background: #00f6ff;
  border: none;
  border-radius: 5px;
  color: #000;
  font-weight: bold;
  cursor: pointer;
  transition: all 0.3s ease;
}

.controls button:hover {
  background: #00d9e6;
  transform: translateY(-2px);
}

.chart-info {
  display: flex;
  gap: 20px;
  padding: 10px 15px;
  background: rgba(0, 246, 255, 0.1);
  border-radius: 5px;
  margin-bottom: 15px;
  font-size: 14px;
}

.status-active {
  color: #4caf50;
}

.status-inactive {
  color: #666;
}

.status-destroyed {
  color: #f44336;
}

.chart-container {
  width: 100%;
  height: 400px;
  background: rgba(0, 0, 0, 0.3);
  border-radius: 8px;
}
</style>

简历描述模板

基础版(200字)

plain
负责大屏长时间运行稳定性优化,解决内存泄漏导致的页面卡顿和崩溃问题。
建立完善的资源管理机制,统一管理定时器、事件监听器、图表实例等资源,组件销毁时自动清理。
使用Chrome DevTools Memory面板排查内存泄漏,通过Heap Snapshot对比发现并修复7处泄漏点。
实现内存监控系统,实时检测内存使用率,达到90%阈值自动告警,95%自动刷新页面。
优化后大屏可连续运行24小时+,内存占用稳定在200MB以内,页面流畅度显著提升。

进阶版(350字)

plain
主导大屏长时间运行内存管理方案设计,解决7*24小时运行导致的内存泄漏、页面卡顿、
自动崩溃等稳定性问题。

排查与修复:
1. 使用Chrome DevTools排查
   - Heap Snapshot对比: 发现7处泄漏点
   - Retainers分析: 定位引用链路
   - Allocation Timeline: 找出频繁分配对象

2. 常见泄漏修复
   - 定时器泄漏: 封装资源管理器统一清理
   - 事件监听泄漏: onUnmounted自动移除
   - 图表实例泄漏: ECharts.dispose()正确销毁
   - 闭包引用泄漏: 及时释放大对象引用
   - Canvas泄漏: 清空画布并释放上下文

3. 内存监控系统
   - 实时监控: 每10秒检测内存使用率
   - 分级告警: 90%警告/95%严重/触发刷新
   - 可视化图表: Canvas绘制内存趋势曲线
   - 快照对比: 保存历史快照便于分析

4. 长期运行优化
   - 定时刷新: 24小时自动刷新页面
   - 心跳检查: 30秒健康检查,3次失败告警
   - 任务调度: RequestIdleCallback空闲执行
   - 降级策略: 内存不足时关闭非核心功能

解决难点:
- ECharts多实例泄漏: 封装Hooks统一管理生命周期
- WebSocket未关闭: onUnmounted主动关闭连接
- 全局变量累积: LRU算法限制缓存大小

项目成果: 内存占用从初始100MB增长到稳定200MB(之前会涨到800MB+),
24小时连续运行无崩溃,页面流畅度提升60%。

高级版(500字)

plain
担任大屏系统稳定性优化技术负责人,系统性解决长时间运行导致的内存泄漏问题,
建立完整的内存管理体系,确保7*24小时稳定运行。

【问题背景】
项目上线初期,大屏运行4-6小时后出现明显卡顿,8-10小时后浏览器崩溃。
监控数据显示内存从初始100MB持续增长至800MB+,严重影响用户体验和系统可用性。

【排查诊断】
使用Chrome DevTools Memory工具进行系统性排查:

1. Heap Snapshot对比分析
   - 采集运行0h/2h/4h/6h四个时间点快照
   - 对比发现Detached DOM Nodes持续增长(2000+个)
   - Array对象异常累积(50000+个元素)
   - 定位到7处主要泄漏点

2. Allocation Timeline时间线
   - 记录5分钟内存分配过程
   - 发现定时器回调频繁创建对象
   - EventListener数量异常(500+个)

3. Retainers引用链分析
   - 追踪对象无法回收的原因
   - 发现多处全局变量持有引用
   - 闭包函数捕获大对象

【技术方案】

1. 资源生命周期管理
   封装ResourceManager统一管理:
   - 定时器: registerTimer注册,自动清理
   - 事件监听: registerListener注册,自动移除
   - 图表实例: registerResource注册,自动dispose
   - WebSocket: onUnmounted主动关闭

2. 图表实例管理
   ECharts多实例泄漏是重灾区:
   - 封装useECharts Hooks
   - 组件卸载自动dispose()
   - resize监听正确清理
   - 数据更新使用setOption而非重建

3. 内存实时监控
   基于Performance.memory API:

监控指标:

  • usedJSHeapSize: 已用堆内存
  • jsHeapSizeLimit: 堆内存上限
  • 使用率: used/limit

告警策略:

  • 80%: 记录日志
  • 90%: 前端告警
  • 95%: 自动刷新
plain
4. 任务调度优化
- RequestIdleCallback: 非紧急任务空闲执行
- 任务队列: 批量处理减少内存分配
- 优先级队列: 重要任务优先
- 可暂停恢复: 页面隐藏时暂停

5. 自动恢复机制
- 24小时定时刷新
- 内存阈值触发刷新
- SessionStorage保存状态
- 刷新后自动恢复

6. 降级策略
内存不足时逐级降级:
Level 1: 关闭动画效果
Level 2: 减少数据刷新频率
Level 3: 关闭非核心功能
Level 4: 强制刷新页面

【解决的关键难点】

难点1: ECharts实例泄漏
表现: 页面有10+个图表,切换页面后内存不释放
原因:
- 组件销毁时未调用dispose()
- resize监听器未移除
- 数据更新时重复创建实例
方案:
- 封装useECharts Hook统一管理
- onUnmounted自动dispose
- 单例模式避免重复创建
效果: 图表内存占用从200MB降至50MB

难点2: 定时器累积泄漏
表现: 定时器数量持续增长,最高达100+个
原因:
- setInterval未clearInterval
- 组件重建时创建新定时器但旧的未清理
- 错误处理未清理定时器
方案:
- ResourceManager统一注册管理
- 自动清理+手动清理双保险
- try-finally确保清理
效果: 定时器数量控制在10个以内

难点3: 事件监听器泄漏
表现: window/document事件监听器持续增长
原因:
- addEventListener未removeEventListener
- 组件销毁时监听器仍在
- 匿名函数无法正确移除
方案:
- 事件处理函数命名化
- WeakMap存储handler引用
- 组件卸载批量移除
效果: 监听器数量从500+降至50以内

【项目成果】
- 运行时长: 4-6小时 → 24小时+连续运行
- 内存占用: 100MB→800MB → 100MB→200MB(稳定)
- 崩溃率: 从50%/天降至0
- 卡顿次数: 减少95%
- 用户投诉: 从每天5+次降至0

方案已沉淀为团队规范,形成《大屏内存管理最佳实践》文档,
在5+个项目中复用,显著提升了系统稳定性。

面试SOP标准回答

Q1: 如何排查内存泄漏?

标准回答(2-3分钟)

"我们项目遇到内存泄漏问题是这样排查的。

首先用Chrome DevTools的Memory面板。我会先记录一个基线快照, 就是页面刚加载完的内存状态,叫Snapshot 1。然后操作页面, 比如打开关闭几次弹窗,切换几次路由,再记录Snapshot 2。 重复这个操作几次,再记录Snapshot 3。

然后对比这几个快照。正常情况下,内存应该是有升有降的, 因为有垃圾回收。但如果发现某些对象一直在增长,从来不降, 那就可能是泄漏了。

DevTools会显示每个对象的Shallow Size和Retained Size。 Shallow Size是对象本身的大小,Retained Size是对象加上它引用的所有对象的大小。 我主要看Retained Size,因为这个能反映真实的内存占用。

找到可疑对象后,点开看Retainers,这个会显示是谁在引用这个对象, 为什么它不能被回收。顺着这个引用链往上找,一般就能定位到代码位置。

我们项目排查出来主要是几个问题:一个是定时器没清除, 一个是ECharts图表实例没dispose,还有一个是全局变量一直在累积数据。

定位到问题后,修复就相对简单了。定时器在onUnmounted里清除, 图表实例调用dispose(),全局变量加个上限控制。

修复后再用同样的方法验证,看内存增长是不是正常了。 我们优化完,内存占用从持续增长变成了稳定在一个范围内波动, 说明泄漏问题解决了。"

追问准备
  • Detached DOM是什么? 答: 就是已经从DOM树移除但还被JS引用的节点。比如你把一个div从页面删除了, 但还有个变量指向它,它就成了Detached DOM,占用内存但不可见。
  • 除了DevTools还有其他工具吗? 答: 还可以用Performance Monitor实时监控内存,或者用第三方工具如MemLab。 但DevTools是最方便的,chrome内置,功能也够用。

Q2: ECharts图表为什么会泄漏?

标准回答(2分钟)

"ECharts泄漏是我们遇到的最严重的问题,因为大屏有很多图表。

主要原因有几个。第一个是图表实例没销毁。ECharts创建图表时会在内部维护很多数据, 包括Canvas上下文、事件监听器、数据缓存这些。如果组件销毁时不调用dispose(), 这些东西都不会被释放,就造成泄漏了。

第二个是resize监听器。很多人会监听window的resize事件来调整图表大小, 但忘了在组件销毁时移除监听器。这样每次创建组件都会添加一个监听器, 监听器越来越多,而且每个监听器都持有图表实例的引用,导致图表无法回收。

第三个是数据更新方式不对。有人每次更新数据都会dispose掉旧图表, 创建新图表,这样反而更容易泄漏。因为创建销毁过程中如果有任何一步没处理好, 就会留下垃圾。

我们的解决方案是封装一个useECharts的Hook。这个Hook会帮你管理图表的完整生命周期。 创建时用echarts.init,更新时用setOption,销毁时用dispose。 而且resize监听器也统一在Hook里管理,组件卸载时自动清理。

用了这个Hook后,图表相关的内存泄漏基本都解决了。我们测试了下, 10个图表切换100次,内存都很稳定,没有泄漏。"

追问准备
  • 为什么不用echarts.dispose? 答: 要用的,这个是正确的销毁方法。错误的做法是直接把实例设为null或者什么都不做, 这样内部资源不会释放。
  • Hook怎么实现的? 答: 很简单,setup里创建图表,onUnmounted里销毁。关键是要把实例、监听器这些都管理好, 不要遗漏。

Q3: 如何监控内存使用?

标准回答(1.5-2分钟)

"我们实现了一个内存监控系统,实时监控大屏的内存状态。

核心是用performance.memory这个API。它有三个属性:usedJSHeapSize是已用的堆内存, totalJSHeapSize是当前分配的总内存,jsHeapSizeLimit是内存上限。

我们每10秒采集一次这些数据,计算使用率,就是used除以limit。 然后设置了几个阈值:80%记录日志,90%前端告警提示用户,95%自动刷新页面。

数据采集后,我们用Canvas画了个实时曲线图,能直观看到内存的变化趋势。 如果发现曲线一直往上涨,不回落,就说明可能有泄漏。

还有个功能是快照对比。用户可以手动拍快照,记录当前的内存状态。 多个快照可以对比,看哪些时间段内存增长异常。

告警这块,如果触发90%告警,我们会在页面上显示一个提示条,告诉用户内存使用过高, 建议刷新。如果到了95%,就直接自动刷新了,避免崩溃。

这个监控系统上线后效果很好,帮我们及时发现了好几个内存问题, 而且大屏运行更稳定了,很少出现崩溃的情况。"

追问准备
  • performance.memory兼容性如何? 答: Chrome支持得很好,其他浏览器可能不支持。我们会先判断if (performance.memory), 不支持就降级,不影响功能。
  • 自动刷新会不会影响用户? 答: 会有一点影响,但比崩溃好。我们会先弹提示,3秒后再刷新,给用户准备时间。 而且刷新前会把状态存到sessionStorage,刷新后自动恢复。

Q4: 24小时自动刷新怎么实现?

标准回答(1.5分钟)

"这个功能主要是为了保证长期运行的稳定性。

实现很简单,就是页面加载时设置一个24小时的定时器。 24小时=24_60_60*1000毫秒,用setTimeout延迟执行。

时间到了之后,我们不是直接刷新,而是先做一些准备工作。 首先把当前的一些状态保存到sessionStorage,比如用户的配置、滚动位置这些。 然后调用location.reload()刷新页面。

页面重新加载后,从sessionStorage读取之前保存的状态,恢复到刷新前的样子。 这样用户感知不到太大的变化。

还有个细节,就是刷新前我们会在sessionStorage里记录一个标记, 表示这是自动刷新而不是用户手动刷新。这样刷新后可以根据这个标记做一些特殊处理, 比如跳过某些初始化步骤,或者记录刷新日志。

这个功能上线后,大屏可以连续运行好几天都没问题。 24小时刷新一次,既保证了稳定性,又不会对用户造成太大影响。"

追问准备
  • 为什么选24小时? 答: 这是个经验值。太短会频繁打断用户,太长可能累积问题太多。 24小时一般是一个工作日,刷新时机也比较好控制。
  • 如果用户正在操作怎么办? 答: 我们会先弹个倒计时提示,比如"页面将在3秒后刷新"。 如果用户正在输入或者有未保存的数据,会先保存再刷新。

难点与亮点分析

难点1: 复杂引用链定位

问题描述: Heap Snapshot显示某个对象持续增长,但Retainers引用链很复杂, 经过多层闭包和第三方库,难以定位到具体的泄漏代码位置。

排查过程

  1. 从Snapshot找到泄漏对象(如Detached DOM)
  2. 查看Retainers引用链
  3. 发现引用链: Window → closure → Array → Object → DOM
  4. 逐层分析每个引用的来源
  5. 使用$0 console调试引用对象
  6. 最终定位到某个全局事件处理函数
解决方案
javascript
// 错误代码
window.addEventListener('resize', () => {
  // 闭包捕获了largeData
  updateChart(largeData)
})

// 修复代码
const handleResize = () => {
  updateChart(getLargeData()) // 动态获取,不持有引用
}
window.addEventListener('resize', handleResize)

onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
})

难点2: 第三方库泄漏处理

问题描述: 项目使用了多个第三方库(ECharts/Three.js/D3.js), 每个库都有自己的资源管理方式,容易遗漏清理导致泄漏。

解决方案: 封装统一的资源管理Hook:

javascript
// useResource Hook
export function useResource() {
  const resources = []

  const register = (resource) => {
    resources.push(resource)
    return resource
  }

  onUnmounted(() => {
    resources.forEach(resource => {
      // 兼容不同库的销毁方法
      if (resource.dispose) resource.dispose()
      if (resource.destroy) resource.destroy()
      if (resource.clear) resource.clear()
      if (resource.remove) resource.remove()
    })
    resources.length = 0
  })

  return { register }
}

// 使用
const { register } = useResource()
const chart = register(echarts.init(el))
const scene = register(new THREE.Scene())

难点3: 内存监控性能开销

问题描述: 频繁采集内存数据(performance.memory)和绘制监控图表, 本身也会消耗性能和内存,可能成为新的性能瓶颈。

优化方案

  1. 降低采集频率: 10秒→30秒(非生产环境可更频繁)
  2. 使用RAF绘制图表,与浏览器渲染同步
  3. 数据点限制: 最多保留60个(5分钟数据)
  4. 离屏Canvas: 复杂图形预绘制
  5. 按需启动: 只在需要时开启监控

效果: 监控系统自身开销从3%CPU降至<1%, 内存占用<5MB,对业务无明显影响。

亮点1: 自动化资源管理

设计思路: 不依赖开发者手动清理资源,而是通过框架层自动管理, 降低出错概率。

实现方案

javascript
// 自动注册系统
const autoCleanup = {
  timers: new Set(),
  listeners: new WeakMap(),

  setInterval(fn, delay) {
    const timer = setInterval(fn, delay)
    this.timers.add(timer)
    return timer
  },

  addEventListener(target, event, handler) {
    target.addEventListener(event, handler)
    if (!this.listeners.has(target)) {
      this.listeners.set(target, [])
    }
    this.listeners.get(target).push({ event, handler })
  },

  cleanup() {
    this.timers.forEach(timer => clearInterval(timer))
    this.listeners.forEach((list, target) => {
      list.forEach(({ event, handler }) => {
        target.removeEventListener(event, handler)
      })
    })
  }
}

亮点2: 渐进式降级策略

分级降级

javascript
const performanceLevel = {
  FULL: 4,      // 全功能
  HIGH: 3,      // 关闭动画
  MEDIUM: 2,    // 降低刷新率
  LOW: 1,       // 只保留核心功能
  MINIMAL: 0    // 最小化运行
}

function adjustPerformance(memoryUsage) {
  if (memoryUsage > 0.95) {
    setLevel(performanceLevel.MINIMAL)
  } else if (memoryUsage > 0.90) {
    setLevel(performanceLevel.LOW)
  } else if (memoryUsage > 0.85) {
    setLevel(performanceLevel.MEDIUM)
  } else if (memoryUsage > 0.80) {
    setLevel(performanceLevel.HIGH)
  } else {
    setLevel(performanceLevel.FULL)
  }
}

真实项目经验表达

问题发现过程

正确示范: "我们这个大屏项目刚上线的时候,用户反馈说页面用一段时间就会很卡, 有时候还会直接崩溃。刚开始我们以为是服务器的问题,后来发现不是, 是前端的内存泄漏。

我用Chrome的Task Manager看了一下,发现页面的内存占用一直在涨, 从刚打开的100MB,4个小时后涨到了500MB,8个小时后就800MB+了。 而且内存占用从来不降,说明肯定有东西没被垃圾回收。

然后我就用DevTools的Memory面板开始排查。先记录了一个初始快照, 然后操作页面,又记录了几个快照。对比快照发现,有几类对象一直在增长: Detached DOM、Array、EventListener这些。

我点开Detached DOM看了下,发现有2000多个。这肯定不正常, 说明有DOM节点被删除了但还被JavaScript引用着,导致无法回收..."

解决方案表达

正确示范: "定位到问题后,我开始一个个修复。

第一个是定时器的问题。我发现很多组件里都用了setInterval, 但在onUnmounted里没有clearInterval。每次组件重新创建,就会多一个定时器, 最后定时器越来越多。我的解决方法是封装了一个资源管理器, 所有定时器都注册到这个管理器,组件销毁时统一清理。

第二个是ECharts的问题。我们页面有10多个图表,每个图表都是个ECharts实例。 最开始我没调用dispose(),导致图表实例一直在内存里。后来我在每个图表组件的 onUnmounted里加了dispose(),问题就解决了。为了方便,我还封装了一个 useECharts的Hook,专门管理图表生命周期。

第三个是事件监听器的问题。window的resize、scroll这些事件, 很多地方都在监听,但都没remove。我统计了一下,最多的时候有500多个监听器。 我的做法是把所有监听器也注册到资源管理器,统一管理。

修复完这些问题,我又测试了一遍,内存占用就正常了。 现在页面运行24小时,内存也就200MB左右,很稳定..."


总结

核心技术要点

  1. Chrome DevTools内存排查
  2. 统一资源生命周期管理
  3. 实时内存监控与告警
  4. 自动刷新与状态恢复
  5. 渐进式性能降级

项目价值

  • 运行时长: 4-6小时 → 24小时+
  • 内存占用: 100MB→800MB → 100MB→200MB
  • 崩溃率: 50%/天 → 0
  • 用户投诉: 每天5+次 → 0

可扩展方向

  • 机器学习预测内存趋势
  • 分布式内存监控平台
  • 自动化泄漏检测工具
  • 内存快照云端分析