在绘制到屏幕之前修改内存画布会严重降低JavaScript性能。

4

我注意到如果我在内存中有大量的画布,修改每个画布然后再绘制到屏幕上会极大地降低我的机器性能。即使画布很小,修改也是微小的。

以下是我能想到的最牵强的示例:

var { canvas, ctx } = generateCanvas();
ctx.strokeStyle = "#000";

var images = [];
for (var i = 0; i < 500; i++) {
  images.push(generateCanvas(50, "red"));
}

var fps = 0,
  lastFps = new Date().getTime();
requestAnimationFrame(draw);

function draw() {
  requestAnimationFrame(draw);

  var modRects = document.getElementById("mod-rects").checked;
  var drawRects = document.getElementById("draw-rects").checked;

  ctx.clearRect(0, 0, 500, 500);
  ctx.strokeRect(0, 0, 500, 500);

  fps++;
  if (new Date().getTime() - lastFps > 1000) {
    console.clear();
    console.log(fps);
    fps = 0;
    lastFps = new Date().getTime();
  }

  images.forEach(img => {
    img.ctx.fillStyle = "yellow";
    if (modRects) img.ctx.fillRect(20, 20, 10, 10);
    if (drawRects) ctx.drawImage(img.canvas, 225, 225);
  });
}

function generateCanvas(size = 500, color = "black") {
  var canvas = document.createElement("canvas");
  canvas.width = canvas.height = size;
  var ctx = canvas.getContext("2d");
  ctx.fillStyle = color;
  ctx.fillRect(0, 0, size, size);

  return {
    canvas,
    ctx
  };
}

function generateCheckbox(name) {
  var div = document.createElement("div");
  var check = document.createElement("input");
  check.type = "checkbox";
  check.id = name;
  var label = document.createElement("label");
  label.for = name;
  label.innerHTML = name;
  div.appendChild(check);
  div.appendChild(label);
  return div;
}

document.body.appendChild(canvas);
document.body.appendChild(generateCheckbox("mod-rects"));
document.body.appendChild(generateCheckbox("draw-rects"));
canvas+div+div { margin-bottom: 20px; }

在此示例中,我们创建了500个大小为50x50的画布。在屏幕上的大画布下面有两个复选框。第一个会导致在这500个画布的每一个上绘制一个小黄色正方形。第二个会将这些画布绘制到大画布上。每秒钟FPS将发布到控制台。当其中一个或两个复选框都被选中时,我没有看到任何性能问题,但是当两个都被选中时,性能会急剧下降。
我的第一个想法是这与每帧发送内存画布到GPU有关。
这里是我正在尝试创建的实际效果。 image 视频:https://youtu.be/Vr6v2oF3G-8 代码:https://github.com/awhipple/base-command-dev/blob/e2c38946cdaf573abff5ded5399c90687ffa76a5/engine/gfx/shapes/Particle.js 我的最终目标是能够平滑地过渡画布的颜色。我在上面的代码链接中使用globalCompositeOperation =“source-in”和fillRect()来实现这一点。

2
很可能将较小的图像组合到更大的图像中是通过GPU完成的,但在画布上绘制是由CPU完成的。将这些脏图像传输到GPU内存会很慢... - AKX
1
我认为@AKX说得很对。听起来非常类似于https://dev59.com/lrzpa4cB1Zd3GeqPK3BZ请注意,只有Chrome似乎显示这种行为。至于你在github仓库中的代码,目前只是快速阅读了一下,但不要每帧调用多次getImageData,而是只使用完整画布调用一次,并从该单个ImageData中读取多个像素。此外,它可能会强制您进行大量重写,但最好将相似的绘图调用(即具有相同颜色样式的调用)批处理到单个路径中,并仅调用一次`fill()`而不是几个`fillStyle=color;fillRect()`。 - Kaiido
1
如果你真的需要性能,就使用WebGL... - AKX
我正在尝试构建一个高性能的通用2D游戏引擎。听起来我应该开始研究WebGL了。感谢@AKX的提示。 - Argatron
1
这里是包含最多信息的缺陷。基本上,它就是第一条评论和您的“首要想法”中所说的:他们确实在GPU中缓存位图,并且每次重新绘制时,都会使此缓存无效并且需要比正常情况下更长的时间。请注意,在此缺陷之前(https://bugs.chromium.org/p/chromium/issues/detail?id=806313),性能始终很好。因此,我理解为他们牺牲了性能以换取内存。 - Kaiido
显示剩余3条评论
2个回答

0
如前所述,这是一个问题,涉及每一帧向GPU发送数百个画布的开销。当CPU修改画布时,它会被标记为“脏”,并在下次使用时重新发送到GPU。
我找到的解决方法是创建一个包含我的粒子图像网格的大画布。每个粒子对象都对其分配的网格部分进行修改。然后,一旦所有修改都完成,我们开始进行绘制图像调用,根据需要切割较大的画布。
我还需要切换到globalCompositeOperation = "source-atop",以防止每次尝试更改一个粒子时所有其他粒子都被破坏。
代码:https://github.com/awhipple/base-command-dev/blob/2514327c6c30cb9914962d2c8d604f04bfbdbed5/engine/gfx/shapes/Particle.js 示例:http://avocado.whipple.life/

在draw函数中,当this.newRender === true时,可以看到它会排队等待稍后绘制。然后,一旦每个粒子都有机会排队,就会调用static drawQueuedParticles

最终的结果是,这个更大的画布每帧只发送一次到GPU。我在我的Razorblade Pro上看到了性能提升,从15 FPS提高到了60 FPS,使用了2700 RTX GPU和1500个屏幕粒子。


-1

我认为浏览器被优化为一次显示一个或最多几个画布。我打赌每个画布都是单独上传到GPU的,这将比单个画布具有更多的开销。 GPU资源有限,使用大量画布可能会导致纹理和缓冲区针对每个画布重复清除时产生很多翻转。 这个答案WebGL VS Canvas 2D hardware acceleration还声称Chrome在256px以下没有硬件加速画布。

如果你想用精灵来实现粒子效果,最好使用专门为此类事情构建的WebGL库。我有一个很好的经验https://www.pixijs.com/。如果你在做3D方面的话,https://threejs.org/也很受欢迎。虽然可以构建自己的WebGL引擎,但这非常复杂且需要大量工作。你必须考虑向量数学、顶点缓冲区、支持移动GPU、批处理绘制调用等问题。除非你真的有强烈的需求,否则最好使用现有的库。


我找到了关于256像素以下画布的源代码。然而,当更改画布大小时,我没有注意到任何性能断点。 - Argatron
我可能会尝试使用pixijs。我真的很想从头开始做这个,但我以前曾经深入研究过向量数学/ GPU,那是一个深不见底的坑。 - Argatron
1
是的,确实是这样!如果您已经熟悉WebGL或OpenGL,那肯定会更容易些,但您仍然需要做很多工作。但是,有时候如果这是一个兴趣项目,您只是想玩得开心,请去尝试吧。 - frodo2975

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接