canvas性能建议

0 阅读4分钟

性能建议

  • 使用路径:绘制大量相同样式矩形时,用路径批量绘制处理
    • 尽量减少,比如 fillRectstrokeRect 的调用次数
  • 避免频繁样式切换:相同样式的矩形一起绘制
    • 按颜色分组绘制是Canvas性能优化和代码组织中的一个重要技巧。核心思想很简单:把相同颜色的绘制操作集中在一起,避免频繁切换 fillStylestrokeStyle

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);
    });
}

不是所有情况都要严格按颜色分组,需要考虑:

  1. 代码复杂度:分组逻辑是否让代码更难理解?
  2. 绘制顺序:如果图形有重叠且需要特定前后顺序,分组可能破坏z-order
  3. 动态变化:如果颜色频繁变化,维护分组的开销可能超过收益

一个实用原则

先写清晰可读的代码,如果性能不够,用浏览器性能工具(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);

注意事项

  1. 路径有顶点限制:浏览器对单个路径的顶点数量有限制(通常数万个),超大规模需要分批
  2. 相同样式才能批量:不同填充色/描边色的矩形必须分开批次
  3. 路径会累积:记得在每批次前调用 beginPath()
  4. 权衡复杂度:极简单的场景(几个矩形)独立调用更清晰,批量路径适合大量重复图形