Canvas性能优化

最近对 html5小游戏有点兴趣,由于我感受未来这个东西或许是前端一个重要的应用场景,例如如今每到某些节假日,像支付宝、淘宝或者其余的一些 APP可能会给你推送通知,而后点进去就是一个小游戏,基本上点进去的人,只要不是太抵触,都会玩上一玩的,若是要是刚好 get到用户的 G点,还能进一步加强业务,不管是用户体验,仍是对业务的发展,都是一种很不错的提高方式。html

另外,我说的这个 html5小游戏是包括 WebGLWebVR等在内的东西,不只限于游戏,也能够是其余用到相关技术的场景,例如商品图片 360°在线查看这种,之因此从小游戏入手,是由于小游戏须要的技术一应俱全,能把游戏作好,再用相同的技术去作其余的事情,就比较信手拈来了前端

查找资料,发现门道仍是蛮多的,看了一圈下来,决定从基础入手,先从较为简单的 canvas 游戏看起,看了一些相关文章和书籍,发现这个东西虽然用起来很简单,可是真想用好,发挥其该有的能力仍是有点难度的,最好从实战入手html5

因而最近准备写个 canvas小游戏练手,相关 UI素材已经搜集好了,不过俗话说 工欲善其事必先利其器,因为对这方面没什么经验,因此为了不过程当中出现的各类坑点,特意又看了一些相关的踩坑文章,其中性能我感受是必需要注意的地方,并且门道不少,因此整理了一下web

使用 requestNextAnimationFrame进行动画循环

setTimeoutsetInterval并不是是专为连续循环产生的 API,因此可能没法达到流畅的动画表现,故用 requestNextAnimationFrame,可能须要 polyfillcanvas

const raf = window.requestAnimationFrame
  || window.webkitRequestAnimationFrame
  || window.mozRequestAnimationFrame
  || window.oRequestAnimationFrame
  || window.msRequestAnimationFrame
  || function(callback) {
    window.setTimeout(callback, 1000 / 60)
  }
复制代码

利用剪辑区域来处理动画背景或其余不变的图像

若是只是简单动画,那么每一帧动画擦除并重绘画布上全部内容是可取的操做,但若是背景比较复杂,那么可使用 剪辑区域技术,经过每帧较少的绘制来得到更好的性能api

利用剪辑区域技术来恢复上一帧动画所占背景图的执行步骤:数组

  • 调用 context.save(),保存屏幕 canvas的状态
  • 经过调用 beginPath来开始一段新的路径
  • context对象上调用 arc()rect()等方法来设置路径
  • 调用 context.clip()方法,将当前路径设置为屏幕 canvas的剪辑区域
  • 擦除屏幕 canvas中的图像(实际上只会擦除剪辑区域所在的这一块范围)
  • 将背景图像绘制到屏幕 canvas上(绘制操做实际上只会影响剪辑区域所在的范围,因此每帧绘制图像像素数更少)
  • 恢复屏幕 canvas的状态参数,重置剪辑区域

离屏缓冲区(离屏canvas)

先绘制到一个离屏 canvas中,而后再经过 drawImage把离屏 canvas 画到主 canvas中,就是把离屏 canvas当成一个缓存区。把须要重复绘制的画面数据进行缓存起来,减小调用 canvasAPI的消耗浏览器

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,避免由于 jsdom之间存在的关联,致使垃圾回收机制没法正常工做,占用资源缓存

尽可能利用 CSS

背景图

若是有大的静态背景图,直接绘制到 canvas可能并非一个很好的作法,若是能够,将这个大背景图做为 background-image 放在一个 DOM元素上(例如,一个 div),而后将这个元素放到 canvas后面,这样就少了一个 canvas的绘制渲染dom

transform变幻

CSStransform性能优于 canvastransfomr API,由于前者基于能够很好地利用 GPU,因此若是能够,transform变幻请使用 CSS来控制

关闭透明度

建立 canvas上下文的 API存在第二个参数:

canvas.getContext(contextType, contextAttributes)
复制代码

contextType 是上下文类型,通常值都是 2d,除此以外还有 webglwebgl2bitmaprenderer三个值,只不事后面三个浏览器支持度过低,通常不用

contextAttributes 是上下文属性,用于初始化上下文的一些属性,对于不一样的 contextTypecontextAttributes的可取值也不一样,对于经常使用的 2dcontextAttributes可取值有:

  • 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包括 shadowOffsetXshadowOffsetYshadowBlurshadowColor

绘图相关的 API,例如 drawImageputImageData,在绘制时进行缩放操做也会增长耗时时间

固然,上述都是尽可能避免 频繁调用,或用其余手段来控制性能,须要用到的地方确定仍是要用的

避免浮点数的坐标

利用 canvas进行动画绘制时,若是计算出来的坐标是浮点数,那么可能会出现 CSS Sub-pixel的问题,也就是会自动将浮点数值四舍五入转为整数,那么在动画的过程当中,因为元素实际运动的轨迹并非严格按照计算公式获得,那么就可能出现抖动的状况,同时也可能让元素的边缘出现抗锯齿失真 这也是可能影响性能的一方面,由于一直在作没必要要的取证运算

渲染绘制操做不要频繁调用

渲染绘制的 api,例如 stroke()filldrawImage,都是将 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能够看作是一个状态机,例如 fillStyleglobalAlphabeginPath,这些 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游戏彷佛须要的制做时间有点长,天天除了上班以外,剩下的时间实在是很少,不知道何时能搞完,若是一切顺利,我却是还想再用一些游戏引擎,例如 EgretLayaAirCocos Creator 将其重制一遍,以熟悉这些游戏引擎的用法,而后到时候写个系列教程出来……

诶,这么看来,彷佛是要持久战了啊

相关文章
相关标签/搜索