【译】改善HTML5画布的性能


:)  喜欢编程、图形、游戏、运动。


介绍

HTML5画布,它最初来自Apple的一个实验室,现在作为网页二维Immediate渲染模式方面的标准之一,受到广泛地支持。已经有很多开发人员在多媒体、可视化和游戏相关的项目中使用它。而随着应用程序越来越复杂,程序员都会面临性能瓶颈的挑战。

现在有很多优化相关的方案。本文的目的就是将这些方案做一个总结和归纳,让程序员更容易理解它们。这里有一些基本的方案是可以适用于各种平台环境,它们是从计算机图形环境上进行根本性的优化。但是由于现在的浏览器已经开始通过GPU对画布加速,使得其中的一些方法其优化作用越来越小。在本文会适时地指出。

注释:本文不会去讲解HTML5画布的使用方法。如果您想了解可以访问HTML5Rocks上画布相关的文章,或者 《深入HTML5》一书的相关内容以及MDN画布教程。

性能测试

尽管HTML5画布发展迅速,但是我们仍然能够通过JSPerfjsperf.com)测试那些有针对性优化方案所发挥的作用。开发者可以使用JSPerf这个Web应用程序对JavaScript做性能测试。每一个测试都只有一个结果,(例如,画布清除处理),但是为得到相同的结果我们使用了很多不同的方法。JSPerf对每种方法都会运行多次以统计出每秒的操作次数。分数越高意味着优化的越好!

访问者可以通过浏览器在JSPerf性能测试页面上进行测试,然后用Browserscopebrowserscope.org)对结果做标准化处理。本文中的优化技术已经被保存在JSPerf中,你可以得到最新的测试信息,并且了解这些技术是否还实用。另外我写了一个简短的帮助程序,它可以根据测试结果绘制出图表。

文章中所有的性能测试结果都有浏览器的版本信息。由于我们并不知道这些浏览器所处操作系统的信息,也不知道HTML5画布是否被硬件做加速处理,所以这些结果也有其局限性。如果你使用的是Chrome,可以在地址栏中输入about:gpu来查看HTML5画布是否被硬件加速。

预渲染到离屏画布

编写游戏时会经常遇到重复绘制很多相似几何体的情况,对于这种情况你可以通过预渲染场景的大块部分获得性能上的提升。预渲染就是使用一个或多个离屏画布,在这些画布上渲染出一些临时图形,然后将这些已绘制好的画布再渲染到屏幕上。对于熟悉计算机图形学的人来说此技术就是display list

例如,假设我们以每秒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)得到下图:

这项技术对于类似于类似于上例的情况特别有效。另外对于文字渲染也有很好的效果。对于文字渲染可以从下图看到性能上巨大的提升(数据来自jsperf):

从上例可以看出虽然“pre-rendered loose”也使用了预渲染,但是性能仍然比较差。可以看出临时画布大小要在包含图像的情况下尽可能地紧缩到最小,才能得到更高的性能;否则会由于复制画布多余部分而造成性能损失。最合适的画布就是小而简单。

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)。

注:有一种例外:如果要绘制几何体的包围盒十分小(例如,水平或垂直线段),可能分开来渲染会更有效(jsperf):

尽量减少切换绘制状态的次数

HTML5上的画布是通过状态机来实现的,它能够记录填充、描边以及各种样式的设定,例如先前记录的位置点可用于当前的路径绘制上。由于对状态机的操作,会引起开销一部分性能的。所以在性能优化方面,要将主要精力放在图形渲染上。

如果你要填充许多颜色到场景中,按照颜色顺序渲染比按照摆放顺序渲染要更有高效。例如绘制一个细条纹样式,你可以按照设定颜色,绘制条纹,再设定颜色,继续绘制下一个条纹的顺序进行渲染:

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

也可以先绘制奇数条纹,再绘制偶数条纹的顺序进行渲染:

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

下面测试的内容是绘制间隔条纹样式(jsperf):

正如我所述,不停地切换绘制状态的方式会导致性能上的损耗。

只绘制有变化的地方

我们应该尽量减少绘制区域。也就是说,在两次绘制之间仅处理它们不同的地方,这样性能就得有显著的提高。也就是说,每次绘制前没必要将整个屏幕上的内容都清楚掉。

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

保存绘制框的大小和位置,并且只对框内进行操作。

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

接下来的测试内容是绘制一个穿越画布的白点,性能如下图所示(jsperf):

如果熟悉计算机图形学,你应该知道“区域重绘”这个技术,即保存先前渲染的区块,然后在每次渲染前只清理该区块的内容。

此项技术也被运用于基于像素点的渲染上下文上,相关内容可以参考:关于Nintendo模拟器的讨论

对于复杂场景使用多层画布

正如之前所提到的,绘制大块图像需要付出一些性能,所以请尽可能地避免它。不过我们也可以在预绘制阶段使用另一张画布来进行离屏渲染,即使用一张置顶画布。这样就可以在渲染的时候通过GPU将透明的前景画布进行alpha混合,以便得到我们需要的场景。你可能会如下设置,两个画布具有绝对位置,并且其中一个总是在另一个的前面。




在画布前再放置一个的好处在于我们绘制或者清除前景画布时,不必背景不会被改动。如果将游戏或者应用程序中的场景分别放置到前景和背景上,性能会得到更好的提升。下面这个图表中,一种是只在一张画布上做重绘操作,另一种是只在前景上做(jsperf):

你会感觉到背景只是渲染了一次,相对于前景(它是用户关注的重点)而言变化很慢。例如,你可以每次都渲染前景,仅在第n帧时渲染背景。

注:只要合理组织你的应用程序,这种方法对那些由很多画布组成的场景十分有效。

避免使用阴影模糊效果

现在,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):

掌握各种画布清除方法

HTML5画布使用的是immediate模式的绘图方式,每一帧前都需要将场景进行重绘。因此对于使用HTML5画布的应用程序和游戏清除画布是一项重要而又基础的操作。

之前尽量减少切换绘制状态的次数一节中提到了频繁清除整个画布是不可取的。但是当你必须这样做的时候,有两种选择:调用context.clearRect(0, 0, width, height)或者另外使用对那个画布进行这样的操作:canvas.width = canvas.width。

虽然当前调用clearRect比宽度重置的方法更有效。但对于Chrome 14浏览器,有时候重置canvas.width要更快些(jsperf):

还需要注意的是这个方法完全依赖于画布的具体实现方式,而且可能会随着环境的不同而不同。更多信息,请看Simon Sarris关于画布清除的文章

避免浮点坐标

HTML5画布支持子像素渲染,并且你无法关闭它。也就是说如果你使用一个非整数坐标来绘制的话,它会被自动地通过反锯齿做平滑处理。相关内容参见Seb Lee-Delisle的子像素画布性能一文

sub-pixel

如果你不需要一个十分平滑地动画,通过Math.floor或Math.round将坐标转化为整数(jsperf):

为了将浮点坐标转换为整数,你有几种方法可选择。其中最高效的是对目标值加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):

注:如果GPU对画布进行了加速处理,我们就不必在意是否是非整数坐标,因为GPU的处理速度已经可以处理忽略不计了。

通过‘REQUESTANIMATIONFRAME’对动画部分进行优化

推荐使用最新的requestAnimationFrame接口,它适用于不同的浏览器。我们不必让浏览器按照固定帧率进行刷新,只要在一切就绪的时候让浏览器回调你的绘制流程就可以了。另外如果页面并不在前景,浏览器就不会去渲染。

函数requestAnimationFrame会每秒进行60次回调。而事实上它并不能保证每秒60次,所以你需要保存在上一次渲染所花费的时间。请参考如下代码:

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现在只能够在Chrom、Safari和Firefox上调用,具体可以参考此文

当前大多数移动终端的画布的处理都比较慢

我们在讨论一下移动终端。不幸的是在写本文时只有iOS5.0beta版上的Safari 5.1对画布进行了GPU加速。没有GPU辅助,移动设备上的浏览器尽凭借CPU来处理画布,还是比较吃力的。通过JSPerf测试可以看到移动设备和桌面平台对于画布处理有很明显的差距。所以当前还不能在所有设备上完美地进行画布渲染。

总结

总的来讲,本文介绍了一些有用的优化技巧,它们可以帮助你提高那些基于HTML5画布项目的性能。你现在就可以将学到的东西运用起来了。如果你还没有进行相关的开发,可以去Chrome ExperimentsCreative JS上找些灵感来做。

参考


原文出处:http://www.html5rocks.com/en/tutorials/canvas/performance/

希望本文能够帮到你的项目。

有什么不当之处请留言或发邮件,谢谢。

Fork me on GitHub
关于

喜欢编程、图形、游戏、运动。

文章分类 HTML5, JavaScript, 图形, 网页 标签: , , , , , , ,
  • http://network.nature.com/profile/U86F774E4 Kredit ohne Schufa

    Hands down, Apple’s app store wins by a mile. It’s a huge selection of all sorts of apps vs a rather sad selection of a handful for Zune. Microsoft has plans, especially in the realm of games, but I’m not sure I’d want to bet on the future if this aspect is important to you. The iPod is a much better choice in that case.

Info

Ohloh profile for Alex Chi





Github Alex Chi