人人网FED博客

专注于前端技术

EffectiveCanvas – 2Dcanvas优化思路

本文章尝试解决以下问题:
1. canvas的卡顿原因
2. 分析性能瓶颈的思路
Canvas是HTML5的新增特性之一,其允许我们来进行酷炫的图形绘制,从而完成渲染动画的需求。渲染动画的本质就是擦除与重绘,其频率就是帧速率,帧速率决定了留给每帧动画的渲染时间。如果消耗的时间稍微多了一些,就会产生卡顿。

卡顿的原因

卡顿有两种:
我们在一帧中需要完成两件事情 1.计算 2. 渲染
这两个阶段都可以引起卡顿。

1. 由计算过程产生的卡顿,一般是一次性发生的。

计算过程做了什么呢?业务逻辑、坐标计算、对象状态等。

2. 由渲染过程产生的卡顿,一般称之为掉帧,它是周期性发生的。

渲染过程本质上也有两个过程。

1. js调用DOM API 及 Canvas API 进行渲染。

2. GPU渲染进程把渲染后的结果呈现在屏幕上的过程。

那么问题来了,我们以60FPS帧速率为例,每帧16ms,那么在这短短的16ms中你需要完成 :上面的步骤1与2.1。如果不能完成,就会导致浏览器js主线程的阻塞。
但是渲染的开支比计算大几个数量级,那么其实我们的目标就缩小了,除非你的算法非常耗时,那么主要优化渲染性能。

那么时间都去哪儿了?

渲染

我们来看渲染耗费时间都耗哪去了。

API操作

与我们平时感觉到的不同,在canvas中对context赋值的操作是一个非常费时的行为,我简单做了个实验。

而且如果这些行为在save/restore中,会使s/r的时间开销增加600ms+;
虽然和真正的绘制相比,赋值开销是微不足道的,但是也架不住多啊,往往滥用赋值的情况在对象遍历,动画流程中会发生。
在我们对一个精灵对象进行绘制的时候,可能很多实例都是同样的状态,如果我们直接在渲染流程中进行状态赋值,那么就导致了遍历过程中的重复赋值;
在动画流程中,我们善用save/restore可以有效避免重复赋值,但也要小心的是不要滥用s/r,他们也是有开销的。
所以在开发中,为了减少渲染开销,尽量减少对ctx的赋值操作,可以考虑把功能实现转嫁给计算解决。

渲染呈现

减少渲染开销主要是:离屏、双缓冲、分层,这三种方法也经常混淆。

离屏

离屏本质上是我们在内存里又开了一块canvas画布,但是又不把他插入到DOM中去,因为在内存中进行渲染和绘制略快于我们视野内的canvas。(我做了个实验,同样的10万次绘制内存要快1500ms左右)而且通过和drawImage使用能把多次绘制合并为一次绘制,减少了2.2步骤的开支。这就是双缓冲了。

双缓冲

双缓冲机制并不是银弹。
如果你实际去使用双缓冲,把画好的整个离屏canvas绘制到视野canvas,你会发现性能反而比之前差得多。

为什么?

因为各大浏览器厂商在实现canvas元素的时候,已经使用了双缓冲机制, 开发者再实现一遍反而会导致性能的下降,而且这没有任何的好处。

浏览器双缓冲机制

浏览器在处理对Canvas 2D和WebGL的加速时,他们的绘制会直接使用GPU,所以他们一般会有一个[GL FBO](FrameBufferObject)作为自己的缓存(作用上类似离屏),而我们绘制的内容实际上是在FBO上绘制的,你看到的实际上是这个缓冲区的复制。

那么,该怎么用双缓冲呢?
双缓冲的正确兹识应该是配合drawImage将离屏的某个图块复制到视野canvas,这是可以提高绘制效率的。

分层

这在游戏开发中很常见,就像PS一样,不同图层负责不同功能,背景层可以只渲染不擦除,来完成减少开销。
但是,就我日常更多是web开发,这点我觉得有可以改进地方。
我们取其思路,不取其方法。因为web开发背景的变动没有游戏开发那么频繁,再加一块canvas的支出着实是有点杀敌一千自伤八百了。而且我们还有CSS也可以用GPU加速。
所以在web开发中,我们完全可以尝试将背景层提取为一个图片,配合CSS来完成我们的需求,我们可以再离屏中进行绘制背景,通过toDataUrl或者toBlob来将离屏canvas图像转化为png/jpg文件,不过blob兼容性只到IE10,但它可以生成jpg图片。
我们将不动的图片作为背景,真正需要变化的像素在canvas绘制,这样就可以节省一部分渲染开销。

计算

计算优化是个玄学啊(逃…

计算的卡顿一般出现在动画“生命周期”改变的时候,比如动画某时突然需要绘制非常多的动画,这时在一次raf中造成过大计算量,导致超过16ms,阻塞了raf的执行,这就会早一次性的比较长的一次卡顿,和渲染导致的周期性卡顿完全不一样,也是我们判断瓶颈的思路之一。

那么一方面,我们需要把 生成 -计算-渲染 分开,就像玩游戏最开始要有loading一样,我们需要把生产对象的过程从渲染周期中抽离出来,减少动画过程中的开销。
另一方面如果碰到实在需要复杂计算的地方,如果没有兼容性要求,worker是一个不错的选择

web worker

worker兼容到IE10,它可以为你开一条系统进程来处理你的计算,从而根本上的解决计算开销,真是好东西啊。而且worker脚本中拿不到window和document ,就算你想搞事情也不是那么容易的。
需要注意的是,你的worker脚本可以理解在另一个环境跑的,所以在new Worker(“worker.js”)给脚本的时候,这个脚本不通过require和import加载,不过webpack的loader处理,需要单独作为entry生成。

另一方面,WebAssembly 也可以配合使用,那时就可以处理更大的数据量了,不过WASM这东西兼容性太差,可以期待一下。

替代方案:分步

如果你有兼容性要求,那么大计算量就只能分步处理了,
把计算步骤分割到各个raf中,每次完成一部分,来达到消除开销的目的,
但是这真的太麻烦了,一方面带来了代码的臃肿,另一方面自己的功力要求有点高。
如果真的万不得已到了这步…也许好好和你的PM沟通一下更好。

说了很多“术”的东西,我想暨越说说“道”的问题。
真正阻塞我们的不是技术或知识, 而是我们的思维。
当你尝试了各种优化方法还是差强人意的时候,更多该想想是不是又面向过程编码了?
是不是又写着一边喝水一边炒菜的函数,自己还觉得很正常?
本来可以一个对象解决的事情,你却实例了多个?

不能用战术上的勤奋掩盖战略上的懒惰。

 

目录: 页面优化

Tags:

发表评论

电子邮件地址不会被公开。 必填项已用*标注