陈建华的博客
专注web开发
改善HTML5 Canvas的性能
2016-12-15 16:18:13   阅读478次

简介

从苹果的实验开始,HTML5画布是最广泛支持的2D即时模式图形WEB标准。现在许多开发商依靠它来实现各种各样的多媒体项目、可视化和游戏。然而,我们随着构建的应用复杂性的增加,开发人员无意中会遇到性能瓶颈。

有很多不连贯的有关优化画布性能的智慧。本文的目的是为开发人员更容易消化将这些资源整合在一起。本文包括适用于所有计算机图形环境以及Canvas实现改进时会有所变化的特定技术的根本优化。尤其是,由于浏览器厂商实现Canvas GPU加速,一些讨论的概述性能技术将有可能作用较小。 这将在适当情况下加以描述。

请注意,本文不涉及HTML5画布的使用,至于HTML5 Canvas的使用可参阅HTML5Rocks上的画布相关文章,以及个HTML5深入和MDN画布教程。

性能测试

为了应对迅速变化的HTML5画布领域,JSPerf(jsperf.com )测试验证每一项仍然有效的优化建议。JSPerf是一Web应用,允许开发人员编写JavaScript性能测试。每个测试都侧重于要实现目标(例如,清除画布)的结果,并包括达到同样效果的多种方法。在短时间内,JSPerf运行每一方法尽可能多的次数,并给出了统计学上有意义的每秒迭代数字。更高的分数越高结果常常越好!

JSPerf性能测试页面的访问者可在自己的浏览器上运行测试,并让JSPerf存储标准化测试结果到Browserscope (browserscope.org )。因为JSPerf的结果支持本文中的优化技术,可以看到这些技术是否仍然适用的最新信息。我写了一个小帮手应用,呈现结果图表,嵌在本文中。

本文中的所有性能测试结果与浏览器版本相关。这将是一个限制,因为不知道浏览器运行在什么操作系统上,或者更重要的是,运行性能测试时,HTML5画布是否被硬件加速。如果Chrome浏览器的HTML5 画布被硬件加速,可在地址栏输入about:gpu查看相关信息。

离屏画布预渲染

如果你重新绘制多帧画面的类似原语,通常情况下编写一个游戏时,可以通过预渲染场景的大型部件零件大幅提升性能。预渲染是指使用提供临时图像的单独屏幕画布(或画布组),然后渲染屏幕外画布中可见者之一。对于那些熟悉计算机图形学者,这种技术也称之为显示列表。

例如,假设你以每秒60帧的速度重绘Mario,运行动画之前,既可以重绘他的帽子、胡子,和每帧的“M”,或预渲Mario。

没有预渲染:

// canvas, context are defined
function render() {
  drawMario(context);
  requestAnimationFrame(render);
}

预渲染:

var m_canvas = document.createElement('canvas');
m_canvas.width = 64;
m_canvas.height = 64;
var m_context = m_canvas.getContext(‘2d’);
drawMario(m_context);
 
function render() {
  context.drawImage(m_canvas, 0, 0);
  requestAnimationFrame(render);
}

请注意requestAnimationFrame 的使用,这将在后面的章节中更详细讨论。下图说明使用预渲染的性能优势(来自jsperf):

1353311895_5820.png

渲染操作(上例中的drawMario)代价高昂时,这种技术特别有效。一个很好的例子是文本渲染,这是非常昂贵的操作。这里是可预期从预渲染得以显著性能增强的排序操作(来自jsperf):

1353311911_4163.png

然而,观察上面的例子,“松散的预渲染”测试用例表现不佳。当预渲染时,重要的是要确保您的临时画布恰恰适合所绘制图像的边界,否则,离屏渲染的性能增益将被从一画布复制到另一画布时的性能损失所抵消(源目标大小的功能是可变的)。上述测试中的紧缩画布仅仅是较小的:

can2.width = 100;
can2.height = 40;

相对于造成不佳性能的宽松画布为:

can3.width = 300;
can3.height = 100;

批量画布一起调用

由于绘图是一项昂贵的操作,所以,先载入包含大量命令的绘图状态机,然后全部转储到视频缓冲区,是更有效的方式。

例如,绘制多条线时,更有效的方式是创建一包含多条线的路径,并通过一个绘图调用绘制。换言之,不是绘制单独的线条:

for (var i = 0; i < points.length - 1; i++) {
  var p1 = points[i];
  var p2 = points[i+1];
  context.beginPath();
  context.moveTo(p1.x, p1.y);
  context.lineTo(p2.x, p2.y);
  context.stroke();
}

从单一的折线绘制可获得更好的性能:

context.beginPath();
for (var i = 0; i < points.length - 1; i++) {
  var p1 = points[i];
  var p2 = points[i+1];
  context.moveTo(p1.x, p1.y);
  context.lineTo(p2.x, p2.y);
}
context.stroke();

这也适用于HTML5画布。当绘制一复杂路径时,例如,将所有点置入路径会更好,而不是单独渲染线段(jsperf):

1353312327_4621.png

但是请注意,用画布,对此还有一个重要的例外规则:如果绘制所需对象涉及的原语包含小边界矩形框(例如,水平和垂直线),实际上,分别渲染它们效率更高(jsperf):

1353312341_5832.png

避免不必要的画布状态变化

HTML5画布元素是在以状态机之上实现的,该状态机跟踪诸如填充和描边样式,以及组成当前路径的以前点之类的事情。当试图优化图形性能时,它的诱惑力仅仅着重于图形渲染。然而,状态机操作也可以招致性能开销。

例如,如果使用多种填充颜色渲染场景,使用颜色渲染而非画布上的位置要廉价的多。渲染细条纹图案,可以渲染一条纹、改变颜色,以及渲染未来的条纹,等:

for (var i = 0; i < STRIPES; i++) {
  context.fillStyle = (i % 2 ? COLOR1 : COLOR2);
  context.fillRect(i * GAP, 0, GAP, 480);
}

或渲染所有奇数的条纹,然后所有的偶数条纹:

for (var i = 0; i < STRIPES/2; i++) {
  context.fillStyle = COLOR1;
  context.fillRect((i*2) * GAP, 0, GAP, 480);
}
for (var i = 0; i < STRIPES/2; i++) {
  context.fillStyle = COLOR2;
  context.fillRect((i*2+1) * GAP, 0, GAP, 480);
}

下面的性能测试,绘制一个隔行扫描的细条纹图案, 使用这两种方法(jsperf):

1353312354_6793.png

正如预期的那样,隔行扫描方式速度较慢,因为改变状态机是高代价的。


仅渲染屏幕上的不同之处,而非全新的状态


正如预期的那样,在屏幕上少渲染较多渲染廉价。如果重绘之间只有增量的差异,只绘制差异之处可以得到显著的性能提高。换言之,绘制前不是清除整个屏幕:

context.fillRect(0, 0, canvas.width, canvas.height);

跟踪绘制边界框,仅清除其中的东西:

context.fillRect(last.x, last.y, last.width, last.height);

在下面的性能测试说明了这一点,其中包括一个很跨屏幕的白点(jsperf):

1353312374_1791.png

如果熟悉计算机图形学,可能也知道,这种技术称为“重绘区域”,在此以前渲染的边界框被保存,然后清除每个渲染(对象)。

这种技术也适用于基于像素的渲染上下文,一如JavaScript任天堂模拟器谈 中所述。

复杂场景中使用多个分层画布

如前所述,绘制大图像是昂贵的,应尽量避免。除使用另一画布渲染屏幕外,如预渲染部分所示,还可利用彼此之上的画布层。通过使用前台画布的透明度,渲染时可以依靠GPU来复合阿尔法。可对此进行如下设置,使用另一画布之上的两个绝对定位的画布。

<canvas id="bg" width="640" height="480" style="position: absolute; z-index: 0">
</canvas>
<canvas id="fg" width="640" height="480" style="position: absolute; z-index: 1">
</canvas>

只是一个画布的优势,是当绘制或清除前景画布时,不修改背景。如果游戏或多媒体应用可被分为前景和背景时,可以考虑使独立的画布上得到显著的性能提高。下面的图表与本地单画布情况相比,只是重绘和清除前景( jsperf ):

1353312396_8632.png

通常,可以利用不完善的人类视觉优势,背景只渲染一次,或以较前景慢的速度,(这可能吸引大部分用户的注意力)。例如,可以每次渲染前景,但只每N帧渲染背景。

另外请注意,这种做法通常适用于任何数量的复合画布,如果您的应用采用此类结构工作的更好的话。

避免shadowBlur

HTML5的画布像许多其他图形环境,允许开发者模糊原语,但此操作代价可能会非常大:

context.shadowOffsetX = 5;
context.shadowOffsetY = 5;
context.shadowBlur = 4;
context.shadowColor = 'rgba(255, 0, 0, 0.5)';
context.fillRect(20, 20, 150, 100);

下面的性能测试显示了同样的场景,有、无阴影渲染,强烈的性能差异(jsperf):

1353312413_2816.png

掌握各种方法来清除画布

由于HTML5的画布是一个即时模式绘图范例,场景需要明确重新绘制每一帧。正因为如此,对于HTML5画布应用和游戏,清除画布是一个极其重要的操作。

如避免画布状态变化部分所述,清除整个画布往往是不可取的,但如果必须这样做,有两种选择:调用context.clearRect(0, 0, width, height ,或使用一个画布特定的技巧做到这一点:canvas.width = canvas.width;。

在编写代码时,clearRect一般优于宽度复位版本,但在一些使用canvas.width的情况下,Chrome14中的重置操作明显加快(jsperf):

1353312427_8820.png

谨慎使用这个技巧,因为它在很大程度上依赖于底层的画布实现,变化非常大。 欲了解更多信息,请参阅西蒙萨里斯关于“清除画布“的文章。


避免浮点坐标


HTML5的画布支持子像素渲染,没有办法将其关闭。如果不是整数坐标绘制,它会自动使用抗锯齿尝试平滑线条,如下是视觉效果。可参看源自Seb Lee-Delisle的子像素画布文章。

1353312570_9972.png

如果平滑的精灵并非所寻求的效果,使用Math.floor 或 Math.round将坐标转换为整数后可以更快(jsperf):

1353312441_7787.png

要转换浮点坐标为整数,可以使用一些巧妙的技术,其中最高效方法的是给目标数加0.5,然后执行位运算,以消除结果的小数部分。

// With a bitwise or.
rounded = (0.5 + somenum) | 0;
// A double bitwise not.
rounded = ~~ (0.5 + somenum);
// Finally, a left bitwise shift.
rounded = (0.5 + somenum) << 0;

全面的性能故障(jsperf):

1353312900_9619.png

注意,这种优化应不再重要,一旦画布实现GPU加速,将能够很快使用非整数坐标。

使用requestAnimationFrame优化动画

相对较新的requestAnimationFrame aPI,是实现浏览器中交互式应用推荐的方式,而不是命令浏览器以特定的固定打勾速率去渲染;当浏览器可用时,要合理地请浏览器调用你的渲染程序。很好的副作用为,如页面不在前景,浏览器是足够聪明将不做渲染。

requestAnimationFrame回调目的为60 fps的回调幅度,但并不保证能达到,所以需要跟踪其自上次渲染所需的传递时间。看起来可能如下所示:

var x = 100;
var y = 100;
var lastRender = new Date();
function render() {
  var delta = new Date() - lastRender;
  x += delta;
  y += delta;
  context.fillRect(x, y, W, H);
  requestAnimationFrame(render);
}
render();

请注意, requestAnimationFrame适用于画布上以及其他渲染技术,如WebGL。

编写代码时,此API只适用于Chrome、Safari、Firefox,所以应该使用这一代码片段。

大多数移动画布实现运行缓慢

来谈谈移动应用。不幸的是,在编写代码时,只有IOS 5.0beta版运行Safari浏览器5.1包含GPU加速的移动画布实现。没有GPU加速,移动浏览器一般不会有足够强大的CPU适用现代画布应用。上文所述的一些,JSPerf的移动测试执行结果较左面要糟糕的多,大大限制了跨设备的可以期望成功运行应用。

结论

总括来说,这篇文章涵盖了一套全面有用的优化技术,将帮助你开发高性能的基于HTML5画布的项目。现在,在这里已经学到了新的东西,去优化你自己的创作吧,或者,如果目前还没有一个要优化的游戏或应用,检出Chrome浏览器实验和JS创意去寻找灵感。

参考文献

Immediate mode vs. retained mode.
Other HTML5Rocks canvas articles.
The Canvas section of Dive into HTML5.
JSPerf lets developers create JS performance tests.
Browserscope stores browser performance data.
JSPerfView, which renders JSPerf tests as charts.
Simon's blog post on clearing the canvas.
Sebastian's blog post on sub-pixel rendering performance.
Paul's blog post on using the requestAnimationFrame.
Ben's talk about optimizing a JS NES emulator.

英文链接:http://www.html5rocks.com/en/tutorials/canvas/performance/











-----------------------------------------------------
转载请注明来源此处
原地址:#

-----网友评论----
1楼:xiaozl 发表于 2016-12-17 16:35:27
有点看不懂  
-----发表评论----
微网聚博客乐园 ©2014 blog.mn886.net 鲁ICP备14012923号   网站导航