正在加载,请稍候…

JavaScript 性能优化:性能分析、内存泄漏与运行时改进

实用的 JavaScript 性能指南:Chrome DevTools 性能分析、识别内存泄漏、优化热点路径、减少 GC 压力、Web Workers 以及测量

JavaScript 性能优化:性能分析、内存泄漏与运行时改进

不要盲目优化

性能优化的首要原则:先测量。每位经验丰富的工程师都有过花数天优化错误函数而真正瓶颈却在别处的经历。

JavaScript 性能优化始于性能分析工具,而非直觉。本指南将展示如何找到真正的问题并系统地修复它们。

JavaScript 性能优化:性能分析、内存泄漏与运行时改进 插图

Chrome DevTools 性能分析

// 代码中的手动性能分析
console.time('expensive-operation')
const result = expensiveOperation()
console.timeEnd('expensive-operation')
// → "expensive-operation: 234.12ms"

// 使用 performance.mark 和 measure 获取更详细信息
performance.mark('start-render')
renderComplexUI()
performance.mark('end-render')
performance.measure('render-time', 'start-render', 'end-render')

const entries = performance.getEntriesByName('render-time')
console.log(entries[0].duration) // 毫秒

使用 Performance 面板:

  1. 打开 DevTools → Performance
  2. 点击 Record
  3. 执行你想要分析的操作
  4. 停止录制
  5. 查看:火焰图(每个函数的时间)、Bottom-up(总时间)、Call tree

需要关注的关键点:

  • 长任务(主线程上的红色三角形)——阻塞渲染
  • 强制回流/布局(紫色事件)——当 JS 在写入布局后读取时发生
  • 过多的 GC(垃圾回收暂停)
  • React 中不必要的重新渲染(Profiler 面板)

主线程:什么阻塞了它

// ❌ 长时间同步任务——阻塞所有 UI 交互
function processLargeArray(items) {
  return items.map(item => {
    // 模拟每个项目的复杂 CPU 工作
    let result = 0
    for (let i = 0; i < 10000; i++) {
      result += Math.sqrt(i * item.value)
    }
    return result
  })
}

// ✅ 分块处理并让出主线程
async function processLargeArrayChunked(items, chunkSize = 100) {
  const results = []
  
  for (let i = 0; i < items.length; i += chunkSize) {
    const chunk = items.slice(i, i + chunkSize)
    
    // 处理这一块
    const chunkResults = chunk.map(item => {
      let result = 0
      for (let j = 0; j < 10000; j++) {
        result += Math.sqrt(j * item.value)
      }
      return result
    })
    results.push(...chunkResults)
    
    // 每块处理后让出(允许 UI 更新、输入处理)
    await new Promise(resolve => setTimeout(resolve, 0))
    // 或者如果可用,使用 scheduler API:
    // if ('scheduler' in window) await scheduler.yield()
  }
  
  return results
}

Web Workers:真正的并行

// worker.js — 在单独的线程中运行
self.onmessage = function(e) {
  const { items, id } = e.data
  
  // CPU 密集型工作——不阻塞主线程!
  const results = items.map(item => heavyComputation(item))
  
  self.postMessage({ results, id })
}

function heavyComputation(item) {
  let result = 0
  for (let i = 0; i < 1_000_000; i++) {
    result += Math.sqrt(i * item.value)
  }
  return result
}

// main.js
class WorkerPool {
  constructor(size = navigator.hardwareConcurrency) {
    this.workers = Array.from({ length: size }, () => new Worker('./worker.js'))
    this.queue = []
    this.busy = new Set()
    this.callbacks = new Map()
  }
  
  run(data) {
    return new Promise((resolve, reject) => {
      const id = crypto.randomUUID()
      this.callbacks.set(id, { resolve, reject })
      this.queue.push({ data, id })
      this.dispatch()
    })
  }
  
  dispatch() {
    const available = this.workers.find(w => !this.busy.has(w))
    if (!available || this.queue.length === 0) return
    
    const { data, id } = this.queue.shift()
    this.busy.add(available)
    
    available.onmessage = (e) => {
      const { resolve } = this.callbacks.get(e.data.id)
      resolve(e.data.results)
      this.callbacks.delete(e.data.id)
      this.busy.delete(available)
      this.dispatch() // 处理队列中的下一个
    }
    
    available.postMessage({ ...data, id })
  }
}

// 使用
const pool = new WorkerPool(4)
const results = await pool.run({ items: largeArray })

JavaScript 性能优化:性能分析、内存泄漏与运行时改进 插图

内存泄漏:发现并修复

JavaScript 中的内存泄漏通常属于以下之一:

  1. 被遗忘的事件监听器
  2. 持有引用的闭包
  3. 累积数据的全局变量
  4. 未释放的定时器/间隔
// ❌ 经典内存泄漏:事件监听器未移除
class Component {
  constructor(element) {
    this.element = element
    this.data = new Array(1000).fill({ /* 大对象 */ })
    
    // 这个闭包保持了 `this`(及其所有数据)的存活
    // 只要按钮存在!
    document.querySelector('#trigger').addEventListener('click', () => {
      this.update()
    })
  }
  
  destroy() {
    this.element.remove()
    // Bug:addEventListener 未移除——组件仍留在内存中
  }
}

// ✅ 修复:存储引用并在销毁时移除
class Component {
  constructor(element) {
    this.element = element
    this.handleClick = () => this.update() // 存储引用
    document.querySelector('#trigger').addEventListener('click', this.handleClick)
  }
  
  destroy() {
    document.querySelector('#trigger').removeEventListener('click', this.handleClick)
    this.element.remove()
  }
}

// ✅ 更好的方式:AbortController 模式
class Component {
  constructor(element) {
    this.element = element
    this.abortController = new AbortController()
    
    document.querySelector('#trigger').addEventListener(
      'click',
      () => this.update(),
      { signal: this.abortController.signal } // 中止时自动移除
    )
    
    window.addEventListener(
      'resize',
      () => this.onResize(),
      { signal: this.abortController.signal }
    )
  }
  
  destroy() {
    this.abortController.abort() // 一次性移除所有监听器
    this.element.remove()
  }
}

使用 DevTools 检测内存泄漏

// 1. 在操作前拍摄堆快照
// DevTools → Memory → Take snapshot

// 2. 执行可能泄漏的操作
// 例如,导航到某个路由并返回 10 次

// 3. 拍摄另一个堆快照
// 4. 使用 "Comparison" 视图查看增长的内容
// 5. 查找不应存在的保留对象

// 在代码中:使用 WeakMap/WeakRef 作为不应阻止 GC 的缓存
const cache = new WeakMap()  // 不阻止键的 GC

function processElement(el) {
  if (cache.has(el)) return cache.get(el)
  
  const result = expensiveOperation(el)
  cache.set(el, result)
  // 当 el 从 DOM 中移除并被 GC 时,缓存条目也会被清除
  return result
}

// WeakRef:持有引用而不阻止 GC
class ResourceManager {
  constructor(resource) {
    this.ref = new WeakRef(resource)
  }
  
  get() {
    const resource = this.ref.deref()
    if (!resource) {
      // 资源已被 GC——需要重新获取
      return this.acquire()
    }
    return resource
  }
}

减少 GC 压力

垃圾回收暂停会导致卡顿。通过减少分配来降低压力:

// ❌ 每帧分配新对象(60fps = 3600 对象/分钟)
function gameLoop() {
  const position = { x: player.x, y: player.y }  // 每帧新对象
  updateEntities(position)
  requestAnimationFrame(gameLoop)
}

// ✅ 使用对象池重用对象
class ObjectPool {
  constructor(factory, reset, initialSize = 10) {
    this.factory = factory
    this.reset = reset
    this.pool = Array.from({ length: initialSize }, factory)
  }
  
  acquire() {
    return this.pool.pop() ?? this.factory()
  }
  
  release(obj) {
    this.reset(obj)
    this.pool.push(obj)
  }
}

const vecPool = new ObjectPool(
  () => ({ x: 0, y: 0 }),
  (v) => { v.x = 0; v.y = 0 }
)

function gameLoop() {
  const position = vecPool.acquire()
  position.x = player.x
  position.y = player.y
  
  updateEntities(position)
  
  vecPool.release(position) // 返回池中
  requestAnimationFrame(gameLoop)
}
// ❌ 热点路径中的字符串拼接——创建多个中间字符串
function buildQuery(params) {
  let query = ''
  for (const [key, value] of Object.entries(params)) {
    query += key + '=' + encodeURIComponent(value) + '&'  // 多次分配!
  }
  return query.slice(0, -1)
}

// ✅ 数组 join——单次分配
function buildQuery(params) {
  return Object.entries(params)
    .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
    .join('&')
}

JavaScript 性能优化:性能分析、内存泄漏与运行时改进 插图

DOM 性能

// ❌ 导致多次回流/布局
function updateElements(items) {
  items.forEach((item, i) => {
    const el = document.getElementById(`item-${i}`)
    el.style.width = (item.width * 2) + 'px'   // 写入
    el.style.height = el.offsetWidth + 'px'     // 读取(强制回流!)
    el.style.left = (el.getBoundingClientRect().left + 10) + 'px'  // 读取(回流!)
  })
}

// ✅ 批量读取,然后批量写入(不交错)
function updateElements(items) {
  // 先批量读取所有
  const measurements = items.map((item, i) => {
    const el = document.getElementById(`item-${i}`)
    return {
      el,
      width: item.width * 2,
      currentLeft: el.getBoundingClientRect().left,
    }
  })
  
  // 然后批量写入所有
  measurements.forEach(({ el, width, currentLeft }) => {
    el.style.width = width + 'px'
    el.style.left = (currentLeft + 10) + 'px'
  })
}

// ✅ 使用 requestAnimationFrame 进行视觉更新
function updateUI(newData) {
  requestAnimationFrame(() => {
    // 这里的 DOM 写入恰好在浏览器绘制之前发生
    // 与其他 rAF 回调一起批处理
    document.querySelector('#counter').textContent = newData.count
    document.querySelector('#status').className = newData.status
  })
}

// ✅ 大型集合的虚拟列表
// 不要渲染 10,000 个 DOM 节点——只渲染可见的
// 使用 react-window、react-virtual 或 tanstack-virtual

React 特定性能优化

// useMemo:缓存昂贵的计算
const sortedItems = useMemo(
  () => [...items].sort((a, b) => a.name.localeCompare(b.name)),
  [items]  // 仅在 items 变化时重新计算
)

// useCallback:为子组件提供稳定的函数引用
const handleUpdate = useCallback(
  (id: string, value: string) => {
    dispatch({ type: 'UPDATE', id, value })
  },
  [dispatch]  // dispatch 来自 useReducer,是稳定的
)

// React.memo:如果 props 未变则跳过重新渲染
const ExpensiveList = React.memo(function ExpensiveList({ items, onUpdate }) {
  return <ul>{items.map(item => <Item key={item.id} item={item} onUpdate={onUpdate} />)}</ul>
})

// Profiler:测量渲染性能
import { Profiler } from 'react'

<Profiler id="ProductList" onRender={(id, phase, actualDuration) => {
  if (actualDuration > 16) {  // 超过一帧(60fps)
    console.warn(`慢渲染:${id} 耗时 ${actualDuration.toFixed(2)}ms (${phase})`)
  }
}}>
  <ProductList />
</Profiler>

// 状态批处理(React 18+)
// 事件处理程序中的多个 setState 调用会自动批处理
function handleClick() {
  setCount(c => c + 1)   // React 18:这些被批处理为一次渲染
  setFlag(f => !f)       // 不是两次单独的渲染
}

// 对于真正昂贵的初始状态:
const [state, setState] = useState(() => computeInitialState())  // 惰性初始化

测量真实世界性能

// Performance Observer API — 编程方式访问 Web Vitals
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.entryType === 'largest-contentful-paint') {
      console.log('LCP:', entry.startTime)
    }
    if (entry.entryType === 'layout-shift') {
      if (!entry.hadRecentInput) {
        console.log('CLS:', entry.value)
      }
    }
  }
}).observe({ entryTypes: ['largest-contentful-paint', 'layout-shift'] })

// Web Vitals 库(最简单的方法)
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals'

function sendToAnalytics(metric) {
  navigator.sendBeacon('/analytics', JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,  // 'good', 'needs-improvement', 'poor'
    delta: metric.delta,
    id: metric.id,
    navigationType: metric.navigationType,
  }))
}

onCLS(sendToAnalytics)
ONINP(sendToAnalytics)
onLCP(sendToAnalytics)

性能优化在以下情况下最具影响力:从测量开始,识别实际瓶颈,应用针对性修复,并再次测量以确认改进。性能分析工具的存在正是为了避免优化错误的东西。

→ 使用 Benchmark Builder 对不同的代码方法进行基准测试和性能比较。