性能优化是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(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)
}
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()
}
}
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操作 | 批量更新 |