动画性能优化

性能优化是Canvas动画开发的关键环节,良好的优化可以确保动画在各种设备上流畅运行。

为什么需要性能优化

Canvas动画对性能要求较高,优化可以带来以下好处:

好处说明
流畅体验保持稳定的帧率
降低功耗减少CPU/GPU负载
兼容设备适应低端设备
用户满意提升用户体验

渲染优化

减少绘制区域

只绘制需要更新的区域,而不是整个画布。

// 不好的做法:清除整个画布
function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  drawAll()
}

// 好的做法:只清除变化区域
function animate() {
  const oldX = ball.x
  const oldY = ball.y
  
  update()
  
  const padding = 5
  ctx.clearRect(
    Math.min(oldX, ball.x) - ball.radius - padding,
    Math.min(oldY, ball.y) - ball.radius - padding,
    Math.abs(ball.x - oldX) + ball.radius * 2 + padding * 2,
    Math.abs(ball.y - oldY) + ball.radius * 2 + padding * 2
  )
  
  draw()
}

脏矩形技术

class DirtyRectManager {
  constructor() {
    this.rects = []
  }
  
  add(x, y, width, height) {
    this.rects.push({ x, y, width, height })
  }
  
  clear() {
    this.rects.forEach(rect => {
      ctx.clearRect(rect.x, rect.y, rect.width, rect.height)
    })
    this.rects = []
  }
  
  merge() {
    if (this.rects.length <= 1) return
    
    let minX = Infinity, minY = Infinity
    let maxX = -Infinity, maxY = -Infinity
    
    this.rects.forEach(rect => {
      minX = Math.min(minX, rect.x)
      minY = Math.min(minY, rect.y)
      maxX = Math.max(maxX, rect.x + rect.width)
      maxY = Math.max(maxY, rect.y + rect.height)
    })
    
    this.rects = [{ x: minX, y: minY, width: maxX - minX, height: maxY - minY }]
  }
}

const dirtyRects = new DirtyRectManager()

function animate() {
  dirtyRects.clear()
  update()
  render()
  requestAnimationFrame(animate)
}

批量绘制

减少状态切换,批量绘制相同类型的图形。

// 不好的做法:频繁切换颜色
function drawBalls(balls) {
  balls.forEach(ball => {
    ctx.fillStyle = ball.color
    ctx.beginPath()
    ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2)
    ctx.fill()
  })
}

// 好的做法:按颜色分组绘制
function drawBallsOptimized(balls) {
  const groups = {}
  
  balls.forEach(ball => {
    if (!groups[ball.color]) {
      groups[ball.color] = []
    }
    groups[ball.color].push(ball)
  })
  
  Object.entries(groups).forEach(([color, group]) => {
    ctx.fillStyle = color
    group.forEach(ball => {
      ctx.beginPath()
      ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2)
      ctx.fill()
    })
  })
}

批量绘制演示

批量绘制优化

离屏Canvas

什么是离屏Canvas

离屏Canvas(Offscreen Canvas)是在内存中创建的Canvas,用于预渲染复杂内容。

基本用法

const offscreenCanvas = document.createElement('canvas')
offscreenCanvas.width = 200
offscreenCanvas.height = 200
const offCtx = offscreenCanvas.getContext('2d')

// 预渲染静态内容
function prerender() {
  offCtx.fillStyle = '#3498db'
  offCtx.beginPath()
  offCtx.arc(100, 100, 80, 0, Math.PI * 2)
  offCtx.fill()
  
  offCtx.fillStyle = '#fff'
  offCtx.font = 'bold 24px Arial'
  offCtx.textAlign = 'center'
  offCtx.textBaseline = 'middle'
  offCtx.fillText('缓存', 100, 100)
}

// 主循环中直接绘制缓存
function render() {
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  ctx.drawImage(offscreenCanvas, 0, 0)
}

离屏Canvas演示

离屏Canvas缓存

复杂图形缓存

class CachedSprite {
  constructor(drawFn, width, height) {
    this.canvas = document.createElement('canvas')
    this.canvas.width = width
    this.canvas.height = height
    this.ctx = this.canvas.getContext('2d')
    
    drawFn(this.ctx)
  }
  
  draw(targetCtx, x, y) {
    targetCtx.drawImage(this.canvas, x, y)
  }
  
  drawScaled(targetCtx, x, y, scale) {
    targetCtx.drawImage(
      this.canvas,
      x, y,
      this.canvas.width * scale,
      this.canvas.height * scale
    )
  }
}

const sprite = new CachedSprite((ctx) => {
  ctx.fillStyle = '#e74c3c'
  ctx.beginPath()
  for (let i = 0; i < 5; i++) {
    const angle = (i * 4 * Math.PI / 5) - Math.PI / 2
    const x = 50 + Math.cos(angle) * 40
    const y = 50 + Math.sin(angle) * 40
    if (i === 0) ctx.moveTo(x, y)
    else ctx.lineTo(x, y)
  }
  ctx.closePath()
  ctx.fill()
}, 100, 100)

内存管理

对象池

避免频繁创建和销毁对象。

class ObjectPool {
  constructor(factory, initialSize = 10) {
    this.factory = factory
    this.available = []
    this.inUse = new Set()
    
    for (let i = 0; i < initialSize; i++) {
      this.available.push(this.factory())
    }
  }
  
  acquire() {
    let obj = this.available.pop()
    if (!obj) {
      obj = this.factory()
    }
    this.inUse.add(obj)
    return obj
  }
  
  release(obj) {
    if (this.inUse.has(obj)) {
      this.inUse.delete(obj)
      this.available.push(obj)
    }
  }
  
  releaseAll() {
    this.inUse.forEach(obj => {
      this.available.push(obj)
    })
    this.inUse.clear()
  }
}

const particlePool = new ObjectPool(() => ({
  x: 0, y: 0, vx: 0, vy: 0, life: 0, active: false
}), 100)

避免内存泄漏

// 不好的做法:闭包引用导致内存泄漏
function createAnimation() {
  const largeData = new Array(1000000)
  
  return function animate() {
    console.log(largeData.length)  // 保持对largeData的引用
    requestAnimationFrame(animate)
  }
}

// 好的做法:及时清理引用
class AnimationController {
  constructor() {
    this.animationId = null
  }
  
  start() {
    const animate = () => {
      this.update()
      this.animationId = requestAnimationFrame(animate)
    }
    this.animationId = requestAnimationFrame(animate)
  }
  
  stop() {
    if (this.animationId) {
      cancelAnimationFrame(this.animationId)
      this.animationId = null
    }
  }
}

及时清理

class GameManager {
  constructor() {
    this.entities = []
    this.animationId = null
  }
  
  addEntity(entity) {
    this.entities.push(entity)
  }
  
  removeEntity(entity) {
    const index = this.entities.indexOf(entity)
    if (index !== -1) {
      this.entities.splice(index, 1)
    }
  }
  
  destroy() {
    if (this.animationId) {
      cancelAnimationFrame(this.animationId)
      this.animationId = null
    }
    this.entities = []
  }
}

层次剔除

视口剔除

只渲染视口内的对象。

class ViewportCulling {
  constructor(viewport) {
    this.viewport = viewport
  }
  
  isVisible(obj) {
    return obj.x + obj.width > this.viewport.x &&
           obj.x < this.viewport.x + this.viewport.width &&
           obj.y + obj.height > this.viewport.y &&
           obj.y < this.viewport.y + this.viewport.height
  }
  
  filterVisible(objects) {
    return objects.filter(obj => this.isVisible(obj))
  }
}

const culling = new ViewportCulling({ x: 0, y: 0, width: 800, height: 600 })

function render(objects) {
  const visibleObjects = culling.filterVisible(objects)
  visibleObjects.forEach(obj => obj.draw(ctx))
}

空间分区

使用空间分区提高碰撞检测效率。

class SpatialHash {
  constructor(cellSize) {
    this.cellSize = cellSize
    this.grid = new Map()
  }
  
  getKey(x, y) {
    const cellX = Math.floor(x / this.cellSize)
    const cellY = Math.floor(y / this.cellSize)
    return `${cellX},${cellY}`
  }
  
  insert(obj) {
    const key = this.getKey(obj.x, obj.y)
    if (!this.grid.has(key)) {
      this.grid.set(key, [])
    }
    this.grid.get(key).push(obj)
  }
  
  query(x, y, radius) {
    const results = []
    const minCellX = Math.floor((x - radius) / this.cellSize)
    const maxCellX = Math.floor((x + radius) / this.cellSize)
    const minCellY = Math.floor((y - radius) / this.cellSize)
    const maxCellY = Math.floor((y + radius) / this.cellSize)
    
    for (let cx = minCellX; cx <= maxCellX; cx++) {
      for (let cy = minCellY; cy <= maxCellY; cy++) {
        const key = `${cx},${cy}`
        const cell = this.grid.get(key)
        if (cell) {
          results.push(...cell)
        }
      }
    }
    
    return results
  }
  
  clear() {
    this.grid.clear()
  }
}

状态缓存

缓存Canvas状态

class StateCache {
  constructor(ctx) {
    this.ctx = ctx
    this.states = {}
  }
  
  setFillStyle(style) {
    if (this.states.fillStyle !== style) {
      this.ctx.fillStyle = style
      this.states.fillStyle = style
    }
  }
  
  setStrokeStyle(style) {
    if (this.states.strokeStyle !== style) {
      this.ctx.strokeStyle = style
      this.states.strokeStyle = style
    }
  }
  
  setLineWidth(width) {
    if (this.states.lineWidth !== width) {
      this.ctx.lineWidth = width
      this.states.lineWidth = width
    }
  }
  
  reset() {
    this.states = {}
  }
}

避免重复计算

// 不好的做法:每帧重复计算
function animate() {
  const centerX = canvas.width / 2
  const centerY = canvas.height / 2
  const radius = Math.min(centerX, centerY) * 0.8
  
  ctx.beginPath()
  ctx.arc(centerX, centerY, radius, 0, Math.PI * 2)
  ctx.stroke()
  
  requestAnimationFrame(animate)
}

// 好的做法:缓存计算结果
const cachedValues = {
  centerX: 0,
  centerY: 0,
  radius: 0
}

function updateCache() {
  cachedValues.centerX = canvas.width / 2
  cachedValues.centerY = canvas.height / 2
  cachedValues.radius = Math.min(cachedValues.centerX, cachedValues.centerY) * 0.8
}

function animate() {
  ctx.beginPath()
  ctx.arc(cachedValues.centerX, cachedValues.centerY, cachedValues.radius, 0, Math.PI * 2)
  ctx.stroke()
  
  requestAnimationFrame(animate)
}

updateCache()
window.addEventListener('resize', updateCache)

性能监控

性能计时器

class PerformanceTimer {
  constructor() {
    this.times = {}
  }
  
  start(label) {
    this.times[label] = performance.now()
  }
  
  end(label) {
    if (this.times[label]) {
      const elapsed = performance.now() - this.times[label]
      delete this.times[label]
      return elapsed
    }
    return 0
  }
  
  measure(label, fn) {
    this.start(label)
    fn()
    return this.end(label)
  }
}

const timer = new PerformanceTimer()

function animate() {
  timer.start('update')
  update()
  const updateTime = timer.end('update')
  
  timer.start('render')
  render()
  const renderTime = timer.end('render')
  
  console.log(`Update: ${updateTime.toFixed(2)}ms, Render: ${renderTime.toFixed(2)}ms`)
  
  requestAnimationFrame(animate)
}

性能面板

class PerformancePanel {
  constructor(ctx) {
    this.ctx = ctx
    this.metrics = {
      fps: 0,
      frameTime: 0,
      updateTime: 0,
      renderTime: 0,
      memory: 0
    }
    this.frameCount = 0
    this.lastTime = performance.now()
  }
  
  update(metric, value) {
    this.metrics[metric] = value
  }
  
  render() {
    this.frameCount++
    const now = performance.now()
    
    if (now - this.lastTime >= 1000) {
      this.metrics.fps = this.frameCount
      this.frameCount = 0
      this.lastTime = now
      
      if (performance.memory) {
        this.metrics.memory = (performance.memory.usedJSHeapSize / 1048576).toFixed(1)
      }
    }
    
    const ctx = this.ctx
    ctx.save()
    
    ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'
    ctx.fillRect(10, 10, 150, 80)
    
    ctx.fillStyle = '#fff'
    ctx.font = '12px monospace'
    ctx.textAlign = 'left'
    
    const y = 28
    ctx.fillText(`FPS: ${this.metrics.fps}`, 20, y)
    ctx.fillText(`Frame: ${this.metrics.frameTime.toFixed(1)}ms`, 20, y + 16)
    ctx.fillText(`Update: ${this.metrics.updateTime.toFixed(1)}ms`, 20, y + 32)
    ctx.fillText(`Render: ${this.metrics.renderTime.toFixed(1)}ms`, 20, y + 48)
    
    if (this.metrics.memory) {
      ctx.fillText(`Memory: ${this.metrics.memory}MB`, 20, y + 64)
    }
    
    ctx.restore()
  }
}

性能监控演示

性能监控面板

最佳实践总结

渲染优化清单

优化项说明
减少绘制区域只绘制变化的部分
批量绘制减少状态切换
离屏Canvas缓存复杂图形
视口剔除只渲染可见对象
空间分区优化碰撞检测

内存管理清单

优化项说明
对象池复用对象避免GC
及时清理释放不再使用的资源
避免闭包泄漏注意闭包引用
缓存计算避免重复计算

代码优化清单

优化项说明
避免在循环中创建对象提前创建复用
减少函数调用内联简单函数
使用位运算替代部分数学运算
避免频繁DOM操作批量更新