Canvas性能优化

栏目: Html5 · 发布时间: 6年前

内容简介:最近对另外,我说的这个查找资料,发现门道还是蛮多的,看了一圈下来,决定从基础入手,先从较为简单的

最近对 html5 小游戏有点兴趣,因为我感觉将来这个东西或许是前端一个重要的应用场景,例如现在每到某些节假日,像支付宝、淘宝或者其他的一些 APP 可能会给你推送通知,然后点进去就是一个小游戏,基本上点进去的人,只要不是太抵触,都会玩上一玩的,如果要是恰好 get 到用户的 G 点,还能进一步增强业务,无论是用户体验,还是对业务的发展,都是一种很不错的提升方式。

另外,我说的这个 html5 小游戏是包括 WebGLWebVR 等在内的东西,不仅限于游戏,也可以是其他用到相关技术的场景,例如商品图片 360° 在线查看这种,之所以从小游戏入手,是因为小游戏需要的技术包罗万象,能把游戏做好,再用相同的技术去做其他的事情,就比较信手拈来了

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

于是最近准备写个 canvas 小游戏练手,相关 UI 素材已经搜集好了,不过俗话说 工欲善其事必先利其器 ,由于对这方面没什么经验,所以为了避免过程中出现的各种坑点,特地又看了一些相关的踩坑文章,其中性能我感觉是必须要注意的地方,而且门道很多,所以整理了一下

使用 requestNextAnimationFrame 进行动画循环

setTimeoutsetInterval 并非是专为连续循环产生的 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 当成一个缓存区。把需要重复绘制的画面数据进行缓存起来,减少调用 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 的绘制渲染

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 将其重制一遍,以熟悉这些游戏引擎的用法,然后到时候写个系列教程出来……

诶,这么看来,似乎是要持久战了啊


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web Design in a Nutshell

Web Design in a Nutshell

Jennifer Niederst / O'Reilly Media, Inc. / 2006-02-21 / USD 34.99

Are you still designing web sites like it's 1999? If so, you're in for a surprise. Since the last edition of this book appeared five years ago, there has been a major climate change with regard to web......一起来看看 《Web Design in a Nutshell》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试