HTML5 画布最初来自 Apple 的一项实验,是网络上最广泛支持的 2D 直接模式图形标准。现在,许多开发人员都依靠 HTML5 画布开发各种多媒体项目、视觉效果和游戏。但是,随着我们构建的应用的复杂程度不断增加,开发人员会在无意中遇到性能限制。
有关优化画布性能的知识有很多,但并不系统。本文旨在将其中的部分文章整合到更易于开发人员理解的资源中。本文包含适用于所有计算机图形环境和画布专用技术(可能会因画布实施的改进而有所变化)的基本优化。尤其是在浏览器供应商实施画布 GPU 加速时,所述的部分性能技术的效果就会受到影响。我们会适当注明这一情况。
请注意,本文不涉及 HTML5 画布的使用。有关 HTML5 画布的使用,请参阅 HTML5Rocks 上有关画布的文章、深入了解 HTML5 的相关章节和 MDN 画布教程。
为了应对 HTML5 画布的快速变化,您可以通过 JSPerf (jsperf.com) 测试来验证所述的各项优化是否仍然有效。JSPerf 是一款供开发人员编写 JavaScript 性能测试的网络应用。每项测试的关注点均为您尝试实现的某种结果(例如清空画布),且会包含可实现相同结果的多种方法。JSPerf 会在短时间内尽可能多地运行每种方法,并提供具有统计意义的每秒迭代次数。分数越高就表示优化效果越好!
您可以访问 JSPerf 性能测试网页通过浏览器运行测试,并让 JSPerf 将标准测试结果保存在 Browserscope (browserscope.org) 上。本文中的优化技术依据的是 JSPerf 结果,因此您可以回访以查看最新信息,了解相关技术是否仍然适用。我编写了一个简短的辅助应用,用于以图表形式呈现全文中所嵌入的这些结果。
本文中的所有性能结果均与浏览器版本相对应。这最终成为限制条件是因为,我们无从知晓用于运行浏览器的操作系统或者更重要的情况:即运行性能测试时,HTML5 画布是否经过了硬件加速。您可以通过地址栏访问 about:gpu
来查看 Chrome 浏览器的 HTML5 画布是否经过了硬件加速。
如果要将相似图元重复绘制到屏幕的多个帧上(正如编写游戏时的常见情况),您就可以预呈现场景中较大的部分从而显著提升性能。预呈现是指在一张(或多张)离屏画布上呈现临时图片,然后将离屏画布重新呈现到显示的画布上。对于那些熟悉计算机图形的人,此技术也称为显示列表。
例如,假设您要以 60 帧/秒的帧率重复绘制马里奥。您可以在每帧上重复绘制马里奥的帽子、胡子和“M”标志,也可以在播放动画前预呈现他。
无预呈现:
// canvas, context are defined
function render() {
drawMario(context);
requestAnimationFrame(render);
}
预呈现:
var m_canvas = document.createElement_x_x('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):
如果呈现操作(上述示例中的 drawMario
)的性能开销较高,此技术会特别有效。文本呈现就是一个很好的示例,该操作的性能开销非常高。以下图表展示了预呈现此操作可为您带来的大幅度性能提升(数据来自此 jsperf):
但是,从以上示例中可以看出,“宽松预呈现”测试用例的性能较低。请务必在预呈现时确保您的临时画布紧贴在绘制的图片周围,否则离屏呈现的性能提升会由于将一张较大的画布复制到另一张画布上带来的性能损失(因源目标尺寸而有所不同)而抵销。上述测试中的紧凑画布仅需要缩小尺寸:
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),用于说明上述内容:
如果您熟悉计算机图形,可能也会知道“区域重绘”这项技术,即保存先前呈现的边框,然后在每次呈现时清空。
此技术也适用于基于像素呈现的情况,正如此 JavaScript 任天堂模拟器演讲所述。
如上文所述,绘制大图片的性能开销较高,应尽可能避免。除了使用其他画布进行离屏呈现外,如预呈现部分中所述,我们也可以使用相互层叠的画布。我们可以在前景画布中使用透明效果,以便借助 GPU 在呈现时将 alpha 复合在一起。您可以进行如下设置,其中两张绝对定位画布相互重叠。
id="bg" width="640" height="480" style="position: absolute; z-index: 0">
id="fg" width="640" height="480" style="position: absolute; z-index: 1">
此处仅使用一张画布的优势在于,我们无需在绘制或清空前景画布时修改背景。如果您可以将游戏或多媒体应用分为前景和背景,那么请考虑在单独的画布上呈现内容,从而显著提升性能。以下图表对比了单张自然画布用例与仅重复绘制并清空前景的画布用例 (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 画布属于直接模式绘制样式,所以需要在每帧中明确地重复绘制场景。因此,对于 HTML5 画布应用和游戏来说,清空画布是一项很重要的基本操作。
正如避免画布状态更改部分中所述,您通常无需清空整个画布,但如果必须执行该操作,您有两个选择:调用 context.clearRect(0, 0, width, height)
或者使用一种画布专用的技巧:canvas.width = canvas.width
;。
在撰写本文时,clearRect
通常优于宽度重置版本,但某些情况下在 Chrome 浏览器 14 中使用 canvas.width
重置工具的速度明显更快 (jsperf):
请谨慎对待这条提示,因为它很大程度上取决于基础画布实施,且很可能会发生变化。有关详情,请参阅西蒙·萨里斯 (Simon Sarris) 有关清空画布的文章。
HTML5 画布支持子像素呈现,且无法停用。如果您使用非整数的坐标绘制内容,系统就会自动使用抗锯齿功能,尝试对线条进行平滑处理。该功能的视觉效果如下,取自赛博·李·德莱索 (Seb Lee-Delisle) 的这篇子像素画布性能文章:
如果不需要平滑贴图的效果,您可以使用 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 加速,能快速呈现非整数坐标,那么这种优化就没有什么意义了。
推荐使用相对较新的 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.0 测试版上运行的 Safari 5.1 支持经过 GPU 加速的移动画布实施。如果不经过 GPU 加速,移动浏览器通常没有足够强大的 CPU 实施基于画布的最新应用。上述几种 JSPerf 测试在移动设备上的性能比在桌面计算机上低一个数量级,这大大限制了可以成功运行的跨设备应用的类型。
综上所述,本文全面介绍了一系列实用优化技术,这些技术将有助于您开发基于画布的高性能 HTML5 项目。既然您已通过本文了解到了一些新技术,那就着手优化您的精彩作品吧!如果目前没有需要优化的游戏或应用,您也可以参阅 Chrome 浏览器实验和创意 JavaScript 寻找灵感。