Canvas上下文状态切换(如fillStyle、strokeStyle、font等)会带来一定的性能开销。合理管理状态切换可以显著提升渲染性能。
const canvasStateTypes = {
fillStyle: {
type: 'color',
cost: 'medium',
description: '填充颜色或样式'
},
strokeStyle: {
type: 'color',
cost: 'medium',
description: '描边颜色或样式'
},
lineWidth: {
type: 'number',
cost: 'low',
description: '线条宽度'
},
lineCap: {
type: 'enum',
cost: 'low',
description: '线条端点样式'
},
lineJoin: {
type: 'enum',
cost: 'low',
description: '线条连接样式'
},
miterLimit: {
type: 'number',
cost: 'low',
description: '斜接限制'
},
font: {
type: 'string',
cost: 'high',
description: '字体设置'
},
textAlign: {
type: 'enum',
cost: 'low',
description: '文本对齐'
},
textBaseline: {
type: 'enum',
cost: 'low',
description: '文本基线'
},
globalAlpha: {
type: 'number',
cost: 'medium',
description: '全局透明度'
},
globalCompositeOperation: {
type: 'enum',
cost: 'high',
description: '合成操作'
},
shadowColor: {
type: 'color',
cost: 'medium',
description: '阴影颜色'
},
shadowBlur: {
type: 'number',
cost: 'medium',
description: '阴影模糊'
},
shadowOffsetX: {
type: 'number',
cost: 'low',
description: '阴影X偏移'
},
shadowOffsetY: {
type: 'number',
cost: 'low',
description: '阴影Y偏移'
},
lineDash: {
type: 'array',
cost: 'medium',
description: '虚线模式'
}
}
class StateSwitchBenchmark {
constructor(ctx) {
this.ctx = ctx
this.results = []
}
benchmarkFillStyle(iterations = 10000) {
const ctx = this.ctx
const startNoSwitch = performance.now()
ctx.fillStyle = '#ff0000'
for (let i = 0; i < iterations; i++) {
ctx.fillRect(0, 0, 10, 10)
}
const timeNoSwitch = performance.now() - startNoSwitch
const startWithSwitch = performance.now()
for (let i = 0; i < iterations; i++) {
ctx.fillStyle = `rgb(${i % 256}, 0, 0)`
ctx.fillRect(0, 0, 10, 10)
}
const timeWithSwitch = performance.now() - startWithSwitch
return {
operation: 'fillStyle',
iterations,
noSwitchTime: timeNoSwitch.toFixed(2),
withSwitchTime: timeWithSwitch.toFixed(2),
overhead: ((timeWithSwitch - timeNoSwitch) / iterations * 1000).toFixed(4)
}
}
benchmarkFont(iterations = 10000) {
const ctx = this.ctx
const startNoSwitch = performance.now()
ctx.font = '16px Arial'
for (let i = 0; i < iterations; i++) {
ctx.fillText('Test', 0, 10)
}
const timeNoSwitch = performance.now() - startNoSwitch
const startWithSwitch = performance.now()
for (let i = 0; i < iterations; i++) {
ctx.font = `${10 + (i % 20)}px Arial`
ctx.fillText('Test', 0, 10)
}
const timeWithSwitch = performance.now() - startWithSwitch
return {
operation: 'font',
iterations,
noSwitchTime: timeNoSwitch.toFixed(2),
withSwitchTime: timeWithSwitch.toFixed(2),
overhead: ((timeWithSwitch - timeNoSwitch) / iterations * 1000).toFixed(4)
}
}
runAllBenchmarks() {
this.results.push(this.benchmarkFillStyle())
this.results.push(this.benchmarkFont())
return this.results
}
}
class StateManager {
constructor(ctx) {
this.ctx = ctx
this.currentState = {}
this.stateStack = []
this.initStateTracking()
}
initStateTracking() {
const trackedProperties = [
'fillStyle', 'strokeStyle', 'lineWidth', 'lineCap', 'lineJoin',
'miterLimit', 'font', 'textAlign', 'textBaseline', 'globalAlpha',
'globalCompositeOperation', 'shadowColor', 'shadowBlur',
'shadowOffsetX', 'shadowOffsetY'
]
trackedProperties.forEach(prop => {
this.currentState[prop] = this.ctx[prop]
})
}
set(property, value) {
if (this.currentState[property] !== value) {
this.ctx[property] = value
this.currentState[property] = value
return true
}
return false
}
setMultiple(properties) {
let changed = false
Object.entries(properties).forEach(([key, value]) => {
if (this.set(key, value)) {
changed = true
}
})
return changed
}
get(property) {
return this.currentState[property]
}
push() {
this.stateStack.push({ ...this.currentState })
}
pop() {
if (this.stateStack.length > 0) {
const state = this.stateStack.pop()
this.setMultiple(state)
}
}
reset() {
this.ctx.setTransform(1, 0, 0, 1, 0, 0)
this.ctx.globalAlpha = 1
this.ctx.globalCompositeOperation = 'source-over'
this.ctx.fillStyle = '#000000'
this.ctx.strokeStyle = '#000000'
this.ctx.lineWidth = 1
this.ctx.lineCap = 'butt'
this.ctx.lineJoin = 'miter'
this.ctx.miterLimit = 10
this.ctx.font = '10px sans-serif'
this.ctx.textAlign = 'start'
this.ctx.textBaseline = 'alphabetic'
this.ctx.shadowColor = 'rgba(0, 0, 0, 0)'
this.ctx.shadowBlur = 0
this.ctx.shadowOffsetX = 0
this.ctx.shadowOffsetY = 0
this.initStateTracking()
}
getState() {
return { ...this.currentState }
}
restoreState(state) {
this.setMultiple(state)
}
}
class BatchRenderOptimizer {
constructor(ctx) {
this.ctx = ctx
this.stateManager = new StateManager(ctx)
this.batches = []
this.currentBatch = null
}
beginBatch(state) {
this.currentBatch = {
state,
operations: []
}
}
addOperation(operation) {
if (this.currentBatch) {
this.currentBatch.operations.push(operation)
}
}
endBatch() {
if (this.currentBatch) {
this.batches.push(this.currentBatch)
this.currentBatch = null
}
}
flush() {
this.batches.forEach(batch => {
this.stateManager.setMultiple(batch.state)
batch.operations.forEach(op => {
this.executeOperation(op)
})
})
this.batches = []
}
executeOperation(op) {
switch (op.type) {
case 'fillRect':
this.ctx.fillRect(op.x, op.y, op.width, op.height)
break
case 'strokeRect':
this.ctx.strokeRect(op.x, op.y, op.width, op.height)
break
case 'fillText':
this.ctx.fillText(op.text, op.x, op.y)
break
case 'drawImage':
this.ctx.drawImage(op.image, op.x, op.y)
break
case 'arc':
this.ctx.beginPath()
this.ctx.arc(op.x, op.y, op.radius, op.startAngle, op.endAngle)
if (op.fill) {
this.ctx.fill()
} else {
this.ctx.stroke()
}
break
}
}
clear() {
this.batches = []
this.currentBatch = null
}
}
class ColorGroupedRenderer {
constructor(ctx) {
this.ctx = ctx
this.groups = new Map()
}
addRect(x, y, width, height, fillColor, strokeColor = null) {
const fillKey = fillColor || 'transparent'
if (!this.groups.has(fillKey)) {
this.groups.set(fillKey, { fills: [], strokes: new Map() })
}
this.groups.get(fillKey).fills.push({ x, y, width, height })
if (strokeColor) {
const strokeKey = strokeColor
if (!this.groups.get(fillKey).strokes.has(strokeKey)) {
this.groups.get(fillKey).strokes.set(strokeKey, [])
}
this.groups.get(fillKey).strokes.get(strokeKey).push({ x, y, width, height })
}
}
addCircle(x, y, radius, fillColor, strokeColor = null) {
const fillKey = fillColor || 'transparent'
if (!this.groups.has(fillKey)) {
this.groups.set(fillKey, { fills: [], strokes: new Map() })
}
this.groups.get(fillKey).fills.push({ type: 'circle', x, y, radius })
if (strokeColor) {
const strokeKey = strokeColor
if (!this.groups.get(fillKey).strokes.has(strokeKey)) {
this.groups.get(fillKey).strokes.set(strokeKey, [])
}
this.groups.get(fillKey).strokes.get(strokeKey).push({ type: 'circle', x, y, radius })
}
}
render() {
this.groups.forEach((data, fillKey) => {
if (fillKey !== 'transparent') {
this.ctx.fillStyle = fillKey
data.fills.forEach(item => {
if (item.type === 'circle') {
this.ctx.beginPath()
this.ctx.arc(item.x, item.y, item.radius, 0, Math.PI * 2)
this.ctx.fill()
} else {
this.ctx.fillRect(item.x, item.y, item.width, item.height)
}
})
}
data.strokes.forEach((items, strokeKey) => {
this.ctx.strokeStyle = strokeKey
items.forEach(item => {
if (item.type === 'circle') {
this.ctx.beginPath()
this.ctx.arc(item.x, item.y, item.radius, 0, Math.PI * 2)
this.ctx.stroke()
} else {
this.ctx.strokeRect(item.x, item.y, item.width, item.height)
}
})
})
})
this.groups.clear()
}
}
class TextGroupedRenderer {
constructor(ctx) {
this.ctx = ctx
this.groups = new Map()
}
add(text, x, y, options = {}) {
const {
font = '16px Arial',
fillStyle = '#000000',
strokeStyle = null,
lineWidth = 1,
textAlign = 'left',
textBaseline = 'top'
} = options
const key = `${font}|${fillStyle}|${strokeStyle}|${lineWidth}|${textAlign}|${textBaseline}`
if (!this.groups.has(key)) {
this.groups.set(key, {
font,
fillStyle,
strokeStyle,
lineWidth,
textAlign,
textBaseline,
texts: []
})
}
this.groups.get(key).texts.push({ text, x, y })
}
render() {
this.groups.forEach(data => {
this.ctx.font = data.font
this.ctx.textAlign = data.textAlign
this.ctx.textBaseline = data.textBaseline
if (data.strokeStyle) {
this.ctx.strokeStyle = data.strokeStyle
this.ctx.lineWidth = data.lineWidth
data.texts.forEach(item => {
this.ctx.strokeText(item.text, item.x, item.y)
})
}
this.ctx.fillStyle = data.fillStyle
data.texts.forEach(item => {
this.ctx.fillText(item.text, item.x, item.y)
})
})
this.groups.clear()
}
}
class PathBatchProcessor {
constructor(ctx) {
this.ctx = ctx
this.paths = []
this.currentPath = null
}
beginPath(style = {}) {
this.currentPath = {
style,
commands: []
}
}
moveTo(x, y) {
if (this.currentPath) {
this.currentPath.commands.push({ type: 'moveTo', x, y })
}
}
lineTo(x, y) {
if (this.currentPath) {
this.currentPath.commands.push({ type: 'lineTo', x, y })
}
}
arc(x, y, radius, startAngle, endAngle, counterclockwise = false) {
if (this.currentPath) {
this.currentPath.commands.push({
type: 'arc', x, y, radius, startAngle, endAngle, counterclockwise
})
}
}
arcTo(x1, y1, x2, y2, radius) {
if (this.currentPath) {
this.currentPath.commands.push({ type: 'arcTo', x1, y1, x2, y2, radius })
}
}
quadraticCurveTo(cpx, cpy, x, y) {
if (this.currentPath) {
this.currentPath.commands.push({ type: 'quadraticCurveTo', cpx, cpy, x, y })
}
}
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
if (this.currentPath) {
this.currentPath.commands.push({ type: 'bezierCurveTo', cp1x, cp1y, cp2x, cp2y, x, y })
}
}
closePath() {
if (this.currentPath) {
this.currentPath.commands.push({ type: 'closePath' })
}
}
endPath(fill = true, stroke = false) {
if (this.currentPath) {
this.currentPath.fill = fill
this.currentPath.stroke = stroke
this.paths.push(this.currentPath)
this.currentPath = null
}
}
render() {
const styleGroups = new Map()
this.paths.forEach(path => {
const key = JSON.stringify(path.style)
if (!styleGroups.has(key)) {
styleGroups.set(key, [])
}
styleGroups.get(key).push(path)
})
styleGroups.forEach((paths, key) => {
const style = paths[0].style
if (style.fillStyle) this.ctx.fillStyle = style.fillStyle
if (style.strokeStyle) this.ctx.strokeStyle = style.strokeStyle
if (style.lineWidth) this.ctx.lineWidth = style.lineWidth
if (style.lineCap) this.ctx.lineCap = style.lineCap
if (style.lineJoin) this.ctx.lineJoin = style.lineJoin
paths.forEach(path => {
this.ctx.beginPath()
path.commands.forEach(cmd => {
switch (cmd.type) {
case 'moveTo':
this.ctx.moveTo(cmd.x, cmd.y)
break
case 'lineTo':
this.ctx.lineTo(cmd.x, cmd.y)
break
case 'arc':
this.ctx.arc(cmd.x, cmd.y, cmd.radius, cmd.startAngle, cmd.endAngle, cmd.counterclockwise)
break
case 'arcTo':
this.ctx.arcTo(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.radius)
break
case 'quadraticCurveTo':
this.ctx.quadraticCurveTo(cmd.cpx, cmd.cpy, cmd.x, cmd.y)
break
case 'bezierCurveTo':
this.ctx.bezierCurveTo(cmd.cp1x, cmd.cp1y, cmd.cp2x, cmd.cp2y, cmd.x, cmd.y)
break
case 'closePath':
this.ctx.closePath()
break
}
})
if (path.fill) this.ctx.fill()
if (path.stroke) this.ctx.stroke()
})
})
this.paths = []
}
}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas 状态优化 - 性能对比</title>
<style>
body {
margin: 0;
padding: 20px;
background: #1a1a2e;
font-family: system-ui, sans-serif;
}
.container {
max-width: 900px;
margin: 0 auto;
}
h1 {
color: #eee;
text-align: center;
}
.canvas-container {
display: flex;
gap: 20px;
flex-wrap: wrap;
justify-content: center;
}
.canvas-wrapper {
background: #16213e;
border-radius: 8px;
padding: 10px;
}
.canvas-wrapper h3 {
color: #eee;
margin: 0 0 10px;
text-align: center;
}
canvas {
display: block;
background: #0f0f23;
border-radius: 4px;
}
.stats {
color: #00ff00;
font-family: monospace;
font-size: 12px;
margin-top: 10px;
text-align: center;
}
.controls {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 20px;
}
button {
padding: 10px 20px;
background: #e94560;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #ff6b6b;
}
</style>
</head>
<body>
<div class="container">
<h1>状态切换性能对比</h1>
<div class="canvas-container">
<div class="canvas-wrapper">
<h3>频繁状态切换</h3>
<canvas id="unoptimizedCanvas" width="400" height="300"></canvas>
<div class="stats" id="unoptimizedStats">渲染时间: 0ms</div>
</div>
<div class="canvas-wrapper">
<h3>优化状态管理</h3>
<canvas id="optimizedCanvas" width="400" height="300"></canvas>
<div class="stats" id="optimizedStats">渲染时间: 0ms</div>
</div>
</div>
<div class="controls">
<button onclick="runTest()">运行测试</button>
<button onclick="changeCount(1000)">1000对象</button>
<button onclick="changeCount(5000)">5000对象</button>
<button onclick="changeCount(10000)">10000对象</button>
</div>
</div>
<script>
const colors = ['#e94560', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7', '#dfe6e9']
function unoptimizedRender(ctx, objects) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
objects.forEach(obj => {
ctx.fillStyle = obj.color
ctx.strokeStyle = '#fff'
ctx.lineWidth = 2
ctx.font = '12px Arial'
if (obj.type === 'rect') {
ctx.fillRect(obj.x, obj.y, obj.width, obj.height)
ctx.strokeRect(obj.x, obj.y, obj.width, obj.height)
} else {
ctx.beginPath()
ctx.arc(obj.x, obj.y, obj.radius, 0, Math.PI * 2)
ctx.fill()
ctx.stroke()
}
ctx.fillText(obj.id, obj.x, obj.y - 5)
})
}
function optimizedRender(ctx, objects) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
const colorGroups = new Map()
objects.forEach(obj => {
if (!colorGroups.has(obj.color)) {
colorGroups.set(obj.color, [])
}
colorGroups.get(obj.color).push(obj)
})
colorGroups.forEach((objs, color) => {
ctx.fillStyle = color
ctx.strokeStyle = '#fff'
ctx.lineWidth = 2
objs.forEach(obj => {
if (obj.type === 'rect') {
ctx.fillRect(obj.x, obj.y, obj.width, obj.height)
ctx.strokeRect(obj.x, obj.y, obj.width, obj.height)
} else {
ctx.beginPath()
ctx.arc(obj.x, obj.y, obj.radius, 0, Math.PI * 2)
ctx.fill()
ctx.stroke()
}
})
})
ctx.font = '12px Arial'
ctx.fillStyle = '#fff'
objects.forEach(obj => {
ctx.fillText(obj.id, obj.x, obj.y - 5)
})
}
function generateObjects(count) {
const objects = []
for (let i = 0; i < count; i++) {
const isRect = Math.random() > 0.5
objects.push({
id: `#${i}`,
type: isRect ? 'rect' : 'circle',
x: Math.random() * 350 + 25,
y: Math.random() * 250 + 25,
width: isRect ? 30 + Math.random() * 20 : undefined,
height: isRect ? 30 + Math.random() * 20 : undefined,
radius: isRect ? undefined : 15 + Math.random() * 10,
color: colors[Math.floor(Math.random() * colors.length)]
})
}
return objects
}
const unoptimizedCanvas = document.getElementById('unoptimizedCanvas')
const optimizedCanvas = document.getElementById('optimizedCanvas')
const unoptimizedStats = document.getElementById('unoptimizedStats')
const optimizedStats = document.getElementById('optimizedStats')
const unoptimizedCtx = unoptimizedCanvas.getContext('2d')
const optimizedCtx = optimizedCanvas.getContext('2d')
let objectCount = 5000
let objects = generateObjects(objectCount)
function runTest() {
const start1 = performance.now()
unoptimizedRender(unoptimizedCtx, objects)
const time1 = performance.now() - start1
const start2 = performance.now()
optimizedRender(optimizedCtx, objects)
const time2 = performance.now() - start2
unoptimizedStats.textContent = `渲染时间: ${time1.toFixed(2)}ms`
optimizedStats.textContent = `渲染时间: ${time2.toFixed(2)}ms`
}
function changeCount(count) {
objectCount = count
objects = generateObjects(count)
runTest()
}
runTest()
</script>
</body>
</html>
const stateOptimizationPrinciples = {
grouping: {
title: '按状态分组',
description: '将相同状态的绘制操作分组处理',
example: `
ctx.fillStyle = 'red'
rects.forEach(r => ctx.fillRect(r.x, r.y, r.w, r.h))
ctx.fillStyle = 'blue'
circles.forEach(c => {
ctx.beginPath()
ctx.arc(c.x, c.y, c.r, 0, Math.PI * 2)
ctx.fill()
})
`
},
caching: {
title: '缓存状态',
description: '避免重复设置相同的状态',
example: `
class StateCache {
constructor(ctx) {
this.ctx = ctx
this.cache = {}
}
setFillStyle(color) {
if (this.cache.fillStyle !== color) {
this.ctx.fillStyle = color
this.cache.fillStyle = color
}
}
}
`
},
batching: {
title: '批量处理',
description: '收集绘制操作后批量执行',
example: `
renderer.beginBatch({ fillStyle: 'red' })
objects.forEach(obj => renderer.addRect(obj))
renderer.endBatch()
renderer.flush()
`
},
pathOptimization: {
title: '路径优化',
description: '合并相同样式的路径',
example: `
ctx.beginPath()
ctx.fillStyle = 'red'
rects.forEach(r => {
ctx.rect(r.x, r.y, r.w, r.h)
})
ctx.fill()
`
}
}
class StateSwitchMonitor {
constructor(ctx) {
this.ctx = ctx
this.originalMethods = {}
this.switchCount = {}
this.isMonitoring = false
this.trackedProperties = [
'fillStyle', 'strokeStyle', 'lineWidth', 'font',
'globalAlpha', 'globalCompositeOperation'
]
}
start() {
if (this.isMonitoring) return
this.switchCount = {}
this.trackedProperties.forEach(prop => {
this.switchCount[prop] = 0
const descriptor = Object.getOwnPropertyDescriptor(this.ctx, prop)
if (descriptor && descriptor.set) {
this.originalMethods[prop] = descriptor.set
const monitor = this
Object.defineProperty(this.ctx, prop, {
get: function() {
return descriptor.get.call(this)
},
set: function(value) {
monitor.switchCount[prop]++
descriptor.set.call(this, value)
}
})
}
})
this.isMonitoring = true
}
stop() {
if (!this.isMonitoring) return
this.trackedProperties.forEach(prop => {
if (this.originalMethods[prop]) {
const descriptor = Object.getOwnPropertyDescriptor(this.ctx, prop)
Object.defineProperty(this.ctx, prop, {
set: this.originalMethods[prop]
})
}
})
this.isMonitoring = false
}
getReport() {
const total = Object.values(this.switchCount).reduce((a, b) => a + b, 0)
return {
total,
details: this.switchCount,
recommendations: this.generateRecommendations()
}
}
generateRecommendations() {
const recommendations = []
if (this.switchCount.fillStyle > 100) {
recommendations.push('考虑按颜色分组渲染以减少fillStyle切换')
}
if (this.switchCount.font > 50) {
recommendations.push('考虑预渲染文本或按字体分组')
}
if (this.switchCount.globalCompositeOperation > 20) {
recommendations.push('考虑按合成模式分组处理')
}
return recommendations
}
}
避免频繁状态切换是Canvas性能优化的重要手段:
通过合理管理Canvas状态,可以显著减少不必要的API调用,提升渲染性能。