{T}

Canvas 性能优化

本文档介绍 Canvas 应用的性能优化技巧,包括离屏渲染、批量绘制、内存管理等内容。

离屏 Canvas

离屏 Canvas(Offscreen Canvas)是在内存中创建的 Canvas,用于预渲染图形,然后一次性绘制到屏幕上。

创建离屏 Canvas

javascript
const offscreenCanvas = document.createElement('canvas')
const offCtx = offscreenCanvas.getContext('2d')

offscreenCanvas.width = 500
offscreenCanvas.height = 400

预渲染静态内容

javascript
// 在离屏 Canvas 上预渲染
function preRender() {
  offCtx.fillStyle = '#3498db'
  offCtx.fillRect(0, 0, 100, 100)
  
  offCtx.fillStyle = '#e74c3c'
  offCtx.beginPath()
  offCtx.arc(200, 200, 50, 0, Math.PI * 2)
  offCtx.fill()
}

preRender()

// 绘制到主 Canvas
ctx.drawImage(offscreenCanvas, 0, 0)

使用场景

复杂背景

javascript
// 预渲染复杂背景
function createBackground(width, height) {
  const canvas = document.createElement('canvas')
  const ctx = canvas.getContext('2d')
  canvas.width = width
  canvas.height = height
  
  // 绘制网格
  ctx.strokeStyle = '#e0e0e0'
  ctx.lineWidth = 1
  
  for (let x = 0; x < width; x += 20) {
    ctx.beginPath()
    ctx.moveTo(x, 0)
    ctx.lineTo(x, height)
    ctx.stroke()
  }
  
  for (let y = 0; y < height; y += 20) {
    ctx.beginPath()
    ctx.moveTo(0, y)
    ctx.lineTo(width, y)
    ctx.stroke()
  }
  
  return canvas
}

const background = createBackground(800, 600)

// 每帧只需绘制一次
function render() {
  ctx.drawImage(background, 0, 0)
  // 绘制动态内容...
}

缓存文字

javascript
// 缓存文字渲染
const textCache = new Map()

function getCachedText(text, font, color) {
  const key = `${text}-${font}-${color}`
  
  if (!textCache.has(key)) {
    const canvas = document.createElement('canvas')
    const ctx = canvas.getContext('2d')
    
    ctx.font = font
    const metrics = ctx.measureText(text)
    
    canvas.width = metrics.width
    canvas.height = parseInt(font) * 1.2
    
    ctx.font = font
    ctx.fillStyle = color
    ctx.textBaseline = 'top'
    ctx.fillText(text, 0, 0)
    
    textCache.set(key, canvas)
  }
  
  return textCache.get(key)
}

// 使用
const textImg = getCachedText('Hello', '30px Arial', '#333')
ctx.drawImage(textImg, 100, 100)

OffscreenCanvas API

javascript
// 使用 Worker 进行离屏渲染
if (typeof OffscreenCanvas !== 'undefined') {
  const offscreen = new OffscreenCanvas(800, 600)
  const offCtx = offscreen.getContext('2d')
  
  // 在 Worker 中渲染
  const worker = new Worker('renderer.js')
  worker.postMessage({ canvas: offscreen }, [offscreen])
}

批量绘制

减少绘制调用次数可以显著提升性能。

批量路径绘制

javascript
// ❌ 低效:每个图形单独绘制
for (let i = 0; i < 1000; i++) {
  ctx.beginPath()
  ctx.arc(Math.random() * 800, Math.random() * 600, 5, 0, Math.PI * 2)
  ctx.fillStyle = '#3498db'
  ctx.fill()
}

// ✅ 高效:批量绘制
ctx.fillStyle = '#3498db'
ctx.beginPath()
for (let i = 0; i < 1000; i++) {
  ctx.moveTo(Math.random() * 800 + 5, Math.random() * 600)
  ctx.arc(Math.random() * 800, Math.random() * 600, 5, 0, Math.PI * 2)
}
ctx.fill()

批量样式设置

javascript
// ❌ 低效:频繁切换样式
shapes.forEach(shape => {
  ctx.fillStyle = shape.color
  ctx.fillRect(shape.x, shape.y, shape.width, shape.height)
})

// ✅ 高效:按样式分组绘制
const colorGroups = {}
shapes.forEach(shape => {
  if (!colorGroups[shape.color]) {
    colorGroups[shape.color] = []
  }
  colorGroups[shape.color].push(shape)
})

Object.keys(colorGroups).forEach(color => {
  ctx.fillStyle = color
  colorGroups[color].forEach(shape => {
    ctx.fillRect(shape.x, shape.y, shape.width, shape.height)
  })
})

使用 Path2D

javascript
// 创建可复用的路径
const rectPath = new Path2D()
rectPath.rect(10, 10, 100, 80)

// 多次使用
ctx.fillStyle = '#3498db'
ctx.fill(rectPath)

ctx.translate(150, 0)
ctx.fill(rectPath)

// 从 SVG 路径创建
const starPath = new Path2D('M 100 10 L 40 198 L 190 78 L 10 78 L 160 198 Z')
ctx.fill(starPath)

减少重绘

分层渲染

html
<canvas id="background" width="800" height="600" style="position: absolute;"></canvas>
<canvas id="foreground" width="800" height="600" style="position: absolute;"></canvas>

<script>
  const bgCanvas = document.getElementById('background')
  const fgCanvas = document.getElementById('foreground')
  const bgCtx = bgCanvas.getContext('2d')
  const fgCtx = fgCanvas.getContext('2d')
  
  // 静态背景(只绘制一次)
  bgCtx.fillStyle = '#f5f5f5'
  bgCtx.fillRect(0, 0, 800, 600)
  
  // 动态前景(每帧更新)
  function animate() {
    fgCtx.clearRect(0, 0, 800, 600)
    // 绘制动态内容...
    requestAnimationFrame(animate)
  }
</script>

脏矩形渲染

javascript
class DirtyRectRenderer {
  constructor(canvas) {
    this.canvas = canvas
    this.ctx = canvas.getContext('2d')
    this.dirtyRects = []
  }
  
  addDirtyRect(x, y, width, height) {
    this.dirtyRects.push({ x, y, width, height })
  }
  
  clear() {
    this.dirtyRects.forEach(rect => {
      this.ctx.clearRect(rect.x, rect.y, rect.width, rect.height)
    })
    this.dirtyRects = []
  }
}

const renderer = new DirtyRectRenderer(canvas)

// 只重绘变化的区域
function updateSprite(sprite, newX, newY) {
  // 添加旧位置的脏矩形
  renderer.addDirtyRect(sprite.x, sprite.y, sprite.width, sprite.height)
  
  // 更新位置
  sprite.x = newX
  sprite.y = newY
  
  // 添加新位置的脏矩形
  renderer.addDirtyRect(sprite.x, sprite.y, sprite.width, sprite.height)
}

避免频繁清除

javascript
// ❌ 低效:每帧清除整个画布
function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  // 绘制...
}

// ✅ 高效:只清除必要的区域
function animate() {
  // 只清除变化的区域
  objects.forEach(obj => {
    ctx.clearRect(obj.x - 1, obj.y - 1, obj.width + 2, obj.height + 2)
  })
  // 绘制...
}

内存管理

及时释放资源

javascript
// 清空图片缓存
const imageCache = new Map()

function clearImageCache() {
  imageCache.clear()
}

// 使用 WeakMap 自动垃圾回收
const weakCache = new WeakMap()

function cacheImageData(img, data) {
  weakCache.set(img, data)
}

避免内存泄漏

javascript
// ❌ 内存泄漏:事件监听器未移除
class Component {
  constructor(canvas) {
    this.canvas = canvas
    this.canvas.addEventListener('click', this.onClick)
  }
  
  onClick = () => {
    // 处理点击
  }
}

// ✅ 正确:添加移除方法
class Component {
  constructor(canvas) {
    this.canvas = canvas
    this.canvas.addEventListener('click', this.onClick)
  }
  
  onClick = () => {
    // 处理点击
  }
  
  destroy() {
    this.canvas.removeEventListener('click', this.onClick)
  }
}

ImageData 池

javascript
class ImageDataPool {
  constructor() {
    this.pool = []
  }
  
  get(width, height) {
    // 查找可复用的 ImageData
    for (let i = 0; i < this.pool.length; i++) {
      const data = this.pool[i]
      if (data.width === width && data.height === height) {
        return this.pool.splice(i, 1)[0]
      }
    }
    
    // 创建新的 ImageData
    return ctx.createImageData(width, height)
  }
  
  release(imageData) {
    this.pool.push(imageData)
  }
}

const pool = new ImageDataPool()

function processImage() {
  const imageData = pool.get(100, 100)
  // 处理图像...
  pool.release(imageData)
}

高 DPI 支持

设备像素比

javascript
function setupHiDPICanvas(canvas) {
  const dpr = window.devicePixelRatio || 1
  const rect = canvas.getBoundingClientRect()
  
  // 设置实际分辨率
  canvas.width = rect.width * dpr
  canvas.height = rect.height * dpr
  
  // 设置显示尺寸
  canvas.style.width = rect.width + 'px'
  canvas.style.height = rect.height + 'px'
  
  // 缩放上下文
  const ctx = canvas.getContext('2d')
  ctx.scale(dpr, dpr)
  
  return ctx
}

高 DPI 图片

javascript
function loadHiDPIImage(src, dpr = window.devicePixelRatio || 1) {
  return new Promise((resolve, reject) => {
    const img = new Image()
    
    img.onload = () => {
      // 创建高分辨率版本
      const canvas = document.createElement('canvas')
      const ctx = canvas.getContext('2d')
      
      canvas.width = img.width * dpr
      canvas.height = img.height * dpr
      
      ctx.scale(dpr, dpr)
      ctx.drawImage(img, 0, 0)
      
      resolve(canvas)
    }
    
    img.onerror = reject
    img.src = src
  })
}

性能监控

FPS 计数器

javascript
class FPSCounter {
  constructor() {
    this.fps = 0
    this.frames = 0
    this.lastTime = performance.now()
  }
  
  update() {
    this.frames++
    const currentTime = performance.now()
    
    if (currentTime - this.lastTime >= 1000) {
      this.fps = this.frames
      this.frames = 0
      this.lastTime = currentTime
    }
    
    return this.fps
  }
  
  draw(ctx) {
    ctx.fillStyle = 'black'
    ctx.font = '14px Arial'
    ctx.fillText(`FPS: ${this.fps}`, 10, 20)
  }
}

const fpsCounter = new FPSCounter()

function animate() {
  fpsCounter.update()
  fpsCounter.draw(ctx)
  // ...
  requestAnimationFrame(animate)
}

性能分析

javascript
// 使用 Performance API
function measurePerformance(name, fn) {
  const start = performance.now()
  fn()
  const end = performance.now()
  console.log(`${name}: ${(end - start).toFixed(2)}ms`)
}

measurePerformance('Draw shapes', () => {
  // 绘制操作
})

综合示例

优化的粒子系统

javascript
class OptimizedParticleSystem {
  constructor(canvas) {
    this.canvas = canvas
    this.ctx = canvas.getContext('2d')
    this.particles = []
    this.maxParticles = 500
    
    // 离屏 Canvas
    this.offscreen = document.createElement('canvas')
    this.offscreen.width = 10
    this.offscreen.height = 10
    const offCtx = this.offscreen.getContext('2d')
    offCtx.fillStyle = '#3498db'
    offCtx.beginPath()
    offCtx.arc(5, 5, 5, 0, Math.PI * 2)
    offCtx.fill()
  }
  
  add(x, y) {
    if (this.particles.length < this.maxParticles) {
      this.particles.push({
        x, y,
        vx: (Math.random() - 0.5) * 4,
        vy: (Math.random() - 0.5) * 4,
        life: 1
      })
    }
  }
  
  update() {
    for (let i = this.particles.length - 1; i >= 0; i--) {
      const p = this.particles[i]
      p.x += p.vx
      p.y += p.vy
      p.life -= 0.01
      
      if (p.life <= 0) {
        this.particles.splice(i, 1)
      }
    }
  }
  
  draw() {
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
    
    // 批量绘制粒子
    this.particles.forEach(p => {
      this.ctx.globalAlpha = p.life
      this.ctx.drawImage(this.offscreen, p.x - 5, p.y - 5)
    })
    
    this.ctx.globalAlpha = 1
  }
  
  animate() {
    this.update()
    this.draw()
    requestAnimationFrame(() => this.animate())
  }
}

常见问题

1. 动画卡顿

原因: 重绘过多、内存泄漏、复杂计算。

解决方案: 使用离屏 Canvas、减少重绘区域、优化算法。

2. 内存占用高

原因: 大量未释放的资源。

解决方案: 及时释放资源、使用对象池、避免闭包引用。

3. 移动端性能差

原因: 设备性能限制、未适配高 DPI。

解决方案: 降低画质、减少粒子数量、使用硬件加速。


下一步学习


返回Canvas 教程目录 | 上一篇交互与事件