内容简介:最近对另外,我说的这个查找资料,发现门道还是蛮多的,看了一圈下来,决定从基础入手,先从较为简单的
最近对 html5
小游戏有点兴趣,因为我感觉将来这个东西或许是前端一个重要的应用场景,例如现在每到某些节假日,像支付宝、淘宝或者其他的一些 APP
可能会给你推送通知,然后点进去就是一个小游戏,基本上点进去的人,只要不是太抵触,都会玩上一玩的,如果要是恰好 get
到用户的 G
点,还能进一步增强业务,无论是用户体验,还是对业务的发展,都是一种很不错的提升方式。
另外,我说的这个 html5
小游戏是包括 WebGL
、 WebVR
等在内的东西,不仅限于游戏,也可以是其他用到相关技术的场景,例如商品图片 360°
在线查看这种,之所以从小游戏入手,是因为小游戏需要的技术包罗万象,能把游戏做好,再用相同的技术去做其他的事情,就比较信手拈来了
查找资料,发现门道还是蛮多的,看了一圈下来,决定从基础入手,先从较为简单的 canvas
游戏看起,看了一些相关文章和书籍,发现这个东西虽然用起来很简单,但是真想用好,发挥其该有的能力还是有点难度的,最好从实战入手
于是最近准备写个 canvas
小游戏练手,相关 UI
素材已经搜集好了,不过俗话说 工欲善其事必先利其器
,由于对这方面没什么经验,所以为了避免过程中出现的各种坑点,特地又看了一些相关的踩坑文章,其中性能我感觉是必须要注意的地方,而且门道很多,所以整理了一下
使用 requestNextAnimationFrame
进行动画循环
setTimeout
和 setInterval
并非是专为连续循环产生的 API
,所以可能无法达到流畅的动画表现,故用 requestNextAnimationFrame
,可能需要 polyfill
:
const raf = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) } 复制代码
利用剪辑区域来处理动画背景或其他不变的图像
如果只是简单动画,那么每一帧动画擦除并重绘画布上所有内容是可取的操作,但如果背景比较复杂,那么可以使用 剪辑区域 技术,通过每帧较少的绘制来获得更好的性能
利用剪辑区域技术来恢复上一帧动画所占背景图的执行步骤:
-
调用
context.save()
,保存屏幕canvas
的状态 -
通过调用
beginPath
来开始一段新的路径 -
在
context
对象上调用arc()
、rect()
等方法来设置路径 -
调用
context.clip()
方法,将当前路径设置为屏幕canvas
的剪辑区域 -
擦除屏幕
canvas
中的图像(实际上只会擦除剪辑区域所在的这一块范围) -
将背景图像绘制到屏幕
canvas
上(绘制操作实际上只会影响剪辑区域所在的范围,所以每帧绘制图像像素数更少) -
恢复屏幕
canvas
的状态参数,重置剪辑区域
离屏缓冲区(离屏canvas)
先绘制到一个离屏 canvas
中,然后再通过 drawImage
把离屏 canvas
画到主 canvas
中,就是把离屏 canvas
当成一个缓存区。把需要重复绘制的画面数据进行缓存起来,减少调用 canvas
的 API
的消耗
const cacheCanvas = document.createElement('canvas') const cacheCtx = cacheCanvas.getContext('2d') cacheCtx.width = 200 cacheCtx.height = 200 // 绘制到主canvas上 ctx.drawImage(0, 0) 复制代码
虽然离屏 canvas
在绘制之前视野内看不到,但其宽高最好设置得跟缓存元素的尺寸一样,避免资源浪费,也避免绘制多余的不必要图像,同时在 drawImage
时缩放图像也将耗费资源
必要时,可以使用多个离屏 canvas
另外,离屏 canvas
不再使用时,最好把手动将引用重置为 null
,避免因为 js
和 dom
之间存在的关联,导致垃圾回收机制无法正常工作,占用资源
尽量利用 CSS
背景图
如果有大的静态背景图,直接绘制到 canvas
可能并不是一个很好的做法,如果可以,将这个大背景图作为 background-image
放在一个 DOM
元素上(例如,一个 div
),然后将这个元素放到 canvas
后面,这样就少了一个 canvas
的绘制渲染
transform变幻
CSS
的 transform
性能优于 canvas
的 transfomr API
,因为前者基于可以很好地利用 GPU
,所以如果可以, transform
变幻请使用 CSS
来控制
关闭透明度
创建 canvas
上下文的 API
存在第二个参数:
canvas.getContext(contextType, contextAttributes) 复制代码
contextType
是上下文类型,一般值都是 2d
,除此之外还有 webgl
、 webgl2
、 bitmaprenderer
三个值,只不过后面三个浏览器支持度太低,一般不用
contextAttributes
是上下文属性,用于初始化上下文的一些属性,对于不同的 contextType
, contextAttributes
的可取值也不同,对于常用的 2d
, contextAttributes
可取值有:
- alpha
boolean
类型值,表明 canvas
包含一个 alpha
通道. 默认为 true
,如果设置为 false
, 浏览器将认为 canvas
背景总是不透明的, 这样可以加速绘制透明的内容和图片
- willReadFrequently
boolean
类型值,表明是否有重复读取计划。经常使用 getImageData()
,这将迫使软件使用 2D canvas
并节省内存(而不是硬件加速)。这个方案适用于存在属性 gfx.canvas.willReadFrequently
的环境。并设置为 true
(缺省情况下,只有 B2G / Firefox OS
)
支持度低,目前只有 Gecko
内核的浏览器支持,不常用
- storage
string
这样表示使用哪种方式存储(默认为:持久( persistent
))
支持度低,目前只有 Blink
内核的浏览器支持,不常用
上面三个属性,看常用的 alpha
就行了,如果你的游戏使用画布而且不需要透明,当使用 HTMLCanvasElement.getContext()
创建一个绘图上下文时把 alpha
选项设置为 false
,这个选项可以帮助浏览器进行内部优化
const ctx = canvas.getContext('2d', { alpha: false }) 复制代码
尽量不要频繁地调用比较耗时的API
例如
shadow
相关 API
,此类 API
包括 shadowOffsetX
、 shadowOffsetY
、 shadowBlur
、 shadowColor
绘图相关的 API
,例如 drawImage
、 putImageData
,在绘制时进行缩放操作也会增加耗时时间
当然,上述都是尽量避免 频繁 调用,或用其他手段来控制性能,需要用到的地方肯定还是要用的
避免浮点数的坐标
利用 canvas
进行动画绘制时,如果计算出来的坐标是浮点数,那么可能会出现 CSS Sub-pixel
的问题,也就是会自动将浮点数值四舍五入转为整数,那么在动画的过程中,由于元素实际运动的轨迹并不是严格按照计算公式得到,那么就可能出现抖动的情况,同时也可能让元素的边缘出现抗锯齿失真
这也是可能影响性能的一方面,因为一直在做不必要的取证运算
渲染绘制操作不要频繁调用
渲染绘制的 api
,例如 stroke()
、 fill
、 drawImage
,都是将 ctx
状态机里面的状态真实绘制到画布上,这种操作也比较耗费性能
例如,如果你要绘制十条线段,那么先在 ctx
状态机中绘制出十天线段的状态机,再进行一次性的绘制,这将比每条线段都绘制一次要高效得多
for (let i = 0; i < 10; i++) { context.beginPath() context.moveTo(x1[i], y1[i]) context.lineTo(x2[i], y2[i]) // 每条线段都单独调用绘制操作,比较耗费性能 context.stroke() } for (let i = 0; i < 10; i++) { context.beginPath() context.moveTo(x1[i], y1[i]) context.lineTo(x2[i], y2[i]) } // 先绘制一条包含多条线条的路径,最后再一次性绘制,可以得到更好的性能 context.stroke() 复制代码
尽量少的改变状态机 ctx的里状态
ctx
可以看做是一个状态机,例如 fillStyle
、 globalAlpha
、 beginPath
,这些 api
都会改变 ctx
里面对于的状态,频繁改变状态机的状态,是影响性能的
可以通过对操作进行更好的规划,减少状态机的改变,从而得到更加的性能,例如在一个画布上绘制几行文字,最上面和最下面文字的字体都是 30px
,颜色都是 yellowgreen
,中间文字是 20px pink
,那么可以先绘制最上面和最下面的文字,再绘制中间的文字,而非必须从上往下依次绘制,因为前者减少了一次状态机的状态改变
const c = document.getElementById("myCanvas") const ctx = c.getContext("2d") ctx.font = '30 sans-serif' ctx.fillStyle = 'yellowgreen' ctx.fillText("大家好,我是最上面一行", 0, 40) ctx.font = '20 sans-serif' ctx.fillStyle = 'red' ctx.fillText("大家好,我是中间一行", 0, 80) ctx.font = '30 sans-serif' ctx.fillStyle = 'yellowgreen' ctx.fillText("大家好,我是最下面一行", 0, 130) 复制代码
下面的代码实现的效果和上面相同,但是代码量更少,同时比上述代码少改变了一次状态机,性能更加
ctx.font = '30 sans-serif' ctx.fillStyle = 'yellowgreen' ctx.fillText("大家好,我是最上面一行", 0, 40) ctx.fillText("大家好,我是最下面一行", 0, 130) ctx.font = '20 sans-serif' ctx.fillStyle = 'red' ctx.fillText("大家好,我是中间一行", 0, 80) 复制代码
尽量少的调用 canvas API
嗯, canvas
也是通过操纵 js
来绘制的,但是相比于正常的 js
操作,调用 canvas API
将更加消耗资源,所以在绘制之前请做好规划,通过 适量
js
原生计算减少 canvas API
的调用是一件比较划算的事情
当然,请注意 适量
二字,如果减少一行 canvas API
调用的代价是增加十行 js
计算,那这事可能就没必要做了
避免阻塞
在进行某些耗时操作,例如计算大量数据,一帧中包含了太多的绘制状态,大规模的 DOM
操作等,可能会导致页面卡顿,影响用户体验,可以通过以下两种手段:
web worker
web worker
最常用的场景就是大量的频繁计算,减轻主线程压力,如果遇到大规模的计算,可以通过此 API
分担主线程压力,此 API
兼容性已经很不错了,既然 canvas
可以用,那 web worker
也就完全可以考虑使用
分解任务
将一段大的任务过程分解成数个小型任务,使用定时器轮询进行,想要对一段任务进行分解操作,此任务需要满足以下情况:
- 循环处理操作并不要求同步
- 数据并不要求按照顺序处理
分解任务包括两种情形:
- 根据任务总量分配
例如进行一个千万级别的运算总任务,可以将其分解为 10
个百万级别的运算小任务
// 封装 定时器分解任务 函数 function processArray(items, process, callback) { // 复制一份数组副本 var todo=items.concat(); setTimeout(function(){ process(todo.shift()); if(todo.length>0) { // 将当前正在执行的函数本身再次使用定时器 setTimeout(arguments.callee, 25); } else { callback(items); } }, 25); } // 使用 var items=[12,34,65,2,4,76,235,24,9,90]; function outputValue(value) { console.log(value); } processArray(items, outputValue, function(){ console.log('Done!'); }); 复制代码
优点是任务分配模式比较简单,更有控制权,缺点是不好确定小任务的大小
有的小任务可能因为某些原因,会耗费比其他小任务更多的时间,这会造成线程阻塞;而有的小任务可能需要比其他任务少得多的时间,造成资源浪费
- 根据运行时间分配
例如运行一个千万级别的运算总任务,不直接确定分配为多少个子任务,或者分配的颗粒度比较小,在每一个或几个计算完成后,查看此段运算消耗的时间,如果时间小于某个临界值,比如 10ms
,那么就继续进行运算,否则就暂停,等到下一个轮询再进行进行
function timedProcessArray(items, process, callback) { var todo=items.concat(); setTimeout(function(){ // 开始计时 var start = +new Date(); // 如果单个数据处理时间小于 50ms ,则无需分解任务 do { process(todo.shift()); } while (todo.length && (+new Date()-start < 50)); if(todo.length > 0) { setTimeout(arguments.callee, 25); } else { callback(items); } }); } 复制代码
优点是避免了第一种情况出现的问题,缺点是多出了一个时间比较的运算,额外的运算过程也可能影响到性能
总结
我准备做的 canvas
游戏似乎需要的制作时间有点长,每天除了上班之外,剩下的时间实在是不多,不知道什么时候能搞完,如果一切顺利,我倒是还想再用一些游戏引擎,例如 Egret
、 LayaAir
、 Cocos Creator
将其重制一遍,以熟悉这些游戏引擎的用法,然后到时候写个系列教程出来……
诶,这么看来,似乎是要持久战了啊
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 性能优化第一课:性能指标
- 【前端性能优化】vue性能优化
- Golang 性能测试 (2) 性能分析
- 【前端性能优化】02--vue性能优化
- Java性能 -- 性能调优标准
- Java性能 -- 性能调优策略
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。