性能建议
- 使用路径:绘制大量相同样式矩形时,用路径批量绘制处理
- 尽量减少,比如
fillRect和strokeRect的调用次数
- 尽量减少,比如
- 避免频繁样式切换:相同样式的矩形一起绘制
- 按颜色分组绘制是Canvas性能优化和代码组织中的一个重要技巧。核心思想很简单:把相同颜色的绘制操作集中在一起,避免频繁切换
fillStyle或strokeStyle。
- 按颜色分组绘制是Canvas性能优化和代码组织中的一个重要技巧。核心思想很简单:把相同颜色的绘制操作集中在一起,避免频繁切换
1. 基础原理:路径 vs 独立调用
❌ 低效方式(多次调用)
// 绘制100个矩形,每个都单独调用fillRect
for (let i = 0; i < 100; i++) {
ctx.fillStyle = 'blue'; // 实际上只需要设置一次
ctx.fillRect(i * 10, 0, 8, 100); // 100次渲染调用
}
✅ 高效方式(路径批量绘制)
// 先用路径记录所有矩形
ctx.beginPath();
for (let i = 0; i < 100; i++) {
ctx.rect(i * 10, 0, 8, 100); // 只记录路径,不渲染
}
ctx.fillStyle = 'blue';
ctx.fill(); // 一次性渲染所有矩形
2.避免频繁样式切换
为什么需要按颜色分组?
// ❌ 不好的做法:频繁切换颜色
for (let i = 0; i < 100; i++) {
ctx.fillStyle = colors[i % 5]; // 每画一个就换一次
ctx.fillRect(x[i], y[i], 10, 10);
}
// ✅ 好的做法:按颜色分组
// 先画所有红色
ctx.fillStyle = 'red';
for (let i of redIndexes) ctx.fillRect(...);
// 再画所有蓝色
ctx.fillStyle = 'blue';
for (let i of blueIndexes) ctx.fillRect(...);
性能差异:切换 fillStyle 在底层会触发Canvas的状态变更,批量操作可减少这个开销,特别是绘制成百上千个图形时。
什么时候分组效果明显?
| 绘制数量 | 是否分组 | 建议 |
|---|---|---|
| < 50 | 不明显 | 代码可读性优先,不用刻意分组 |
| 50-500 | 轻微提升 | 简单分组即可 |
| 500-5000 | 明显提升 | 推荐分组 |
| > 5000 | 巨大差异 | 必须分组,甚至考虑离屏Canvas |
场景:绘制不同颜色的散点图
// 原始数据:每个点有 x, y, color
const points = [
{ x: 10, y: 20, color: 'red' },
{ x: 30, y: 15, color: 'blue' },
{ x: 50, y: 45, color: 'red' },
{ x: 70, y: 30, color: 'green' },
{ x: 90, y: 55, color: 'blue' },
// ... 可能上百个点
];
// 按颜色分组
const groups = new Map();
points.forEach(point => {
if (!groups.has(point.color)) {
groups.set(point.color, []);
}
groups.get(point.color).push(point);
});
// 按组绘制
for (const [color, pointsOfColor] of groups) {
ctx.fillStyle = color;
pointsOfColor.forEach(point => {
ctx.fillRect(point.x, point.y, 6, 6);
});
}
不是所有情况都要严格按颜色分组,需要考虑:
- 代码复杂度:分组逻辑是否让代码更难理解?
- 绘制顺序:如果图形有重叠且需要特定前后顺序,分组可能破坏z-order
- 动态变化:如果颜色频繁变化,维护分组的开销可能超过收益
一个实用原则:
先写清晰可读的代码,如果性能不够,用浏览器性能工具(Performance tab)定位瓶颈。如果看到大量
fillStyle切换耗时,再按颜色分组优化。
3. ImageData 的正确使用场景
ImageData 不是绘制简单图形的最快方式,因为它涉及 CPU 和 GPU 之间的数据往返,且 JavaScript 逐像素处理是单线程的。它的真正适用场景是图像滤镜、像素级处理等特殊需求:
ImageData 的实际性能瓶颈:
- getImageData() 和 putImageData() 需要从 GPU 读取/写入像素数据,这是昂贵的操作
- 像素操作是在 JavaScript 层面逐像素循环,对于大量像素非常慢
- JavaScript 数组操作比原生 GPU 渲染慢得多
- 如果绘制区域很大,逐像素操作的时间复杂度是 O(n²)
// 适用场景:图像滤镜/像素处理(非简单图形绘制)
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
// 示例:反色滤镜
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i]; // R
data[i + 1] = 255 - data[i + 1]; // G
data[i + 2] = 255 - data[i + 2]; // B
// Alpha 通道不变
}
ctx.putImageData(imageData, 0, 0);
性能对比结论:
- 简单图形(矩形、圆形等):批量路径绘制 > ImageData
- 像素级图像处理:ImageData 是唯一选择
4.更高级的优化:离屏Canvas + 颜色分组
// 创建离屏Canvas预绘制同色图形
function createColorLayer(color, shapes) {
const offscreen = new OffscreenCanvas(800, 600);
const offCtx = offscreen.getContext('2d');
offCtx.fillStyle = color;
shapes.forEach(shape => {
offCtx.fillRect(shape.x, shape.y, shape.w, shape.h);
});
return offscreen;
}
const redShapes = [
{ x: 10, y: 20, w: 6, h: 6 },
{ x: 30, y: 15, w: 6, h: 6 },
{ x: 50, y: 45, w: 6, h: 6 },
{ x: 70, y: 30, w: 6, h: 6 },
{ x: 90, y: 55, w: 6, h: 6}
]
const redLayer = createColorLayer('red', redShapes);
// 主Canvas直接贴图层
ctx.drawImage(redLayer, 0, 0);
注意事项
- 路径有顶点限制:浏览器对单个路径的顶点数量有限制(通常数万个),超大规模需要分批
- 相同样式才能批量:不同填充色/描边色的矩形必须分开批次
- 路径会累积:记得在每批次前调用
beginPath() - 权衡复杂度:极简单的场景(几个矩形)独立调用更清晰,批量路径适合大量重复图形