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 教程目录 | 上一篇:交互与事件