快捷搜索:  汽车  科技

小程序canvas尺寸设置(小程序和H5中canvas卡顿的性能优化方向和实践)

小程序canvas尺寸设置(小程序和H5中canvas卡顿的性能优化方向和实践)我通过3个canvas叠在一起,通过设置每个canvas的 z-index 达到了3个画布还是在同一层的错觉,这样我在requestAnimation中,只需要对 动的图形去做重新绘制就好了,其余的依旧是保持不动 。为什么需要分层渲染, 在游戏中,假设人物的不停地在移动,但是呢背景可能加了很多花里呼哨的元素,但是我在每一次更新的时候,场景本身是不变的,变的只有人物不停的移动,如果每一帧再去重绘不就造成了性能浪费, 这时候分层canvas就出现了 我们先看下一张图你可能就明白了。 function generatePolygon(x y r edges = 3) { const points = [] const detla = 2* Math.PI / edges; for(let i= 0;i<edges;i ) { c

什么是canvas? 首先介绍下canvas, 前端的同学可能很熟悉,举个很简单的例子,
平常用的网页截图、H5游戏、前端动效、可视化图表… 都有canvas 的应用场景, 官方的定义:
canvas是HTML5提供的一种新标签
ie9才开始支持的,canvas是一个矩形区域的画布,可以用JS控制每一个像素在上面绘画。canvas 标签使用 JavaScript
在网页上绘制图像,本身不具备绘图功能。canvas 拥有多种绘制路径、矩形、圆形、字符以及添加图像的方法。
看着很简单,其实canvas这个标签的加入,赋予了我们更多创建惊艳的前端效果的能力。但是你知道他也有性能问题??本篇文章就简单谈一谈Canvas的性能优化。

哪些因素会影响canvas的性能canvas优化的几种方式

我们都知道浏览器上渲染动画 每一秒高达60帧,也就是1秒钟内我们完成60次图像绘制, 也就是每一帧图像的绘制时间其实就是(1000/ 60)。 如果在每一帧动画的时间小于 16.7 ms 辣么就会出现卡顿、丢帧。而canvas 其实是一个指令式绘图系统, 他通过绘图指令来完成绘图操作。
影响canvas两个很关键的因素:
第一个渲染的图形数量多,就是调用绘图指令的次数比较多,
第二个渲染的图形大,就是一次绘图渲染的时间比较长

优化canvas1. 减少绘图指令的调用

这句话怎么理解呢 , 假设你要在场景中画正n变形,这是一个 很常见的需求可能你稍不注意写下了下面这几行代码:

function drawAnyShape(points) { for(let i=0; i<points.length; i ) { const p1 = points[i] const p2 = i=== points.length - 1 ? points[0] : points[i 1] ctx.fillStyle = 'black' ctx.beginPath(); ctx.moveTo(...p1) ctx.lineTo(...p2) ctx.closePath(); ctx.stroke() } }


points 对应的生成多边形的点,代码如下:

function generatePolygon(x y r edges = 3) { const points = [] const detla = 2* Math.PI / edges; for(let i= 0;i<edges;i ) { const theta = i * detla; points.push([x r * Math.sin(theta) y r * Math.cos(theta)]) } return points }


一看这fps低成这个样子,很多人这时候说,你画的图形多,那我只要悄悄的改下代码,就能让fps 回归正常

重写了正多边形的方法:

function drawAnyShape2(points) { ctx.beginPath(); ctx.moveTo(...points[0]); ctx.fillStyle = 'black' for(let i=1; i<points.length; i ) { ctx.lineTo(...points[i]) } ctx.closePath(); ctx.stroke() }

看了下fps 已经成功升到了30fps 这是为什么呢, 第一段我们在循环中去做绘图操作, 循环一次, stoke() 一次,这显然是不合理的,第二个直接把stoke() ,放到循环外,其实就调用了一次,所以我们可以得出减少绘图指令是可以提高canvas的性能的

2.分层渲染

为什么需要分层渲染, 在游戏中,假设人物的不停地在移动,但是呢背景可能加了很多花里呼哨的元素,但是我在每一次更新的时候,场景本身是不变的,变的只有人物不停的移动,如果每一帧再去重绘不就造成了性能浪费, 这时候分层canvas就出现了 我们先看下一张图你可能就明白了。

我通过3个canvas叠在一起,通过设置每个canvas的 z-index 达到了3个画布还是在同一层的错觉,这样我在requestAnimation中,只需要对 动的图形去做重新绘制就好了,其余的依旧是保持不动 。

伪代码

<canvas id="backgroundCanvas" /> <canvas id="peopleActionCanvas" /> const peopleActionCanvas = document.getElementById('peopleActionCanvas'); const backgroundCanvas = document.getElementById('backgroundCanvas'); ​ function draw(){ drawPeopleAction(peopleActionCanvas); if (needDrawBackground) { drawBackground(backgroundCanvas); } requestAnimationFrame(draw); }

一个背景层一个运动层, 在抽象一点,我们什么时候应该去做分层 ,如果画布纯是静态的就没有必要去做分层了, 如果当前有静态有东动态的,你可以逻辑层放在最上面,然后展示层 放在最底下就可以实现所谓的 分层渲染了,但是最好保持在3-5个。

3. 局部渲染

局部渲染的话其实就是调用canvas 的 clip方法。官方文档MDN 对这个方法的使用

CanvasRenderingContext2D.clip() 是 Canvas 2D API 将当前创建的路径设置为当前剪切路径的方法

如何用canvas 画一个1/4圆。

const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); ctx.fillStyle = 'red' ctx.arc(100 100 75 0 Math.PI*2 false); //ctx.clip(); ctx.fillRect(0 0 100 100);

这里填充的时候 没有用clip 画面上应该是一个矩形。

这时候我把clip注释解开来, 矩形变成了一个半圆。 所以clip 这个 api 结合 fillRect 填充 就是实现填充任意图形路径。

canvas 中画了1000 个圆形, 如果你只改一个颜色,那其他999都是不变的 这种浪费是肯定存在性能问题, 如果在做动画效果可想而知,丢帧非常厉害。 这里就可以使用我们上面的api

正确的做法其实就是我们要做局部刷新:

确定改变的元素的包围盒(是否存在相交)
画出路径 然后 clip
最后重新绘制绘制改变的图形
clip() 确定绘制的的裁剪区域,区域之外的图形不能绘制,详情查看 CanvasRenderingContext2D.clip() clearRect(x y width height) 擦除指定矩形内的颜色,查看 CanvasRenderingContext2D.clearRect()

包围盒
用一个框去把图形包围住, 其实在几何中我们叫包围盒 或者是boundingBox。 可以用来快速检测两个图形是否相交, 但是还是不够准确。最好还是用图形算法去解决。 或者游戏中的碰撞检测,都有这个概念。这里讨论的是2d的boudingbox, 还是比较简单的。

虚线框其实就是boundingBox 其实就是根据图形的大小,算出一个矩形边框。理论我们知道了,映射到代码层次, 我们怎么去表达呢? 这里带大家原生实现一下bound2d 类, 其实每个2d图形,都可以去实现。 因为2d图形都是由点组成的,所以只要获得每一个图形的离散点集合, 然后对这些点,去获得一个2d空间的boundBox。

4.离屏CANVAS 和WEBWORKER

我们先说下 什么是离屏canvas???

OffscreenCanvas提供了一个可以脱离屏幕渲染的canvas对象。它在窗口环境和web worker环境均有效。

脱离屏幕渲染的canvas对象,这对我们实际写动画的时候真的有用吗???

想象以下这个场景:如果发现自己在每个动画帧上重复了一些相同的绘制操作,请考虑将其分流到屏幕外的画布上。 然后,您可以根据需要频繁地将屏幕外图像渲染到主画布上,而不必首先重复生成该图像的步骤。由于浏览器是单线程,canvas的计算和渲染其实是在同一个线程的。这就会导致在动画中(有时候很耗时)的计算操作将会导致App卡顿,降低用户体验。

幸运的是 OffscreenCanvas 离屏Canvas可以非常棒的解决这个麻烦!

到目前为止,canvas的绘制功能都与标签绑定在一起,这意味着canvas API和DOM是耦合的。而OffscreenCanvas,正如它的名字一样,通过将Canvas移出屏幕来解耦了DOM和canvas API。

由于这种解耦,OffscreenCanvas的渲染与DOM完全分离了开来,并且比普通canvas速度提升了一些,而这只是因为两者(Canvas和DOM)之间没有同步。但更重要的是,将两者分离后,canvas将可以在Web Worker中使用,即使在Web Worker中没有DOM。这给canvas提供了更多的可能性。

这就离屏canvas 为啥和webworker 这么配的缘故了。

如何创建离屏CANVAS?
创建离屏canvas有两种方式:

一种是通过OffscreenCanvas的构造函数直接创建。比如下面的示例代码:

// 离屏canvas const offscreen = new OffscreenCanvas(200 200); 第二种是使用canvas的transferControlToOffscreen函数获取一个OffscreenCanvas对象,绘制该OffscreenCanvas对象,同时会绘制canvas对象。比如如下代码: const canvas = document.getElementById('canvas'); const offscreen = canvas.transferControlToOffscreen(); 我写了下面这个小demo 验证下到底是不是可靠的 const canvas = document.getElementById('canvas'); // 离屏canvas const offscreen1 = new OffscreenCanvas(200 200); const offscreen2 = canvas.transferControlToOffscreen(); console.error(offscreen1 offscreen2 '222')

离屏canvas怎么与主线程的canvas通信呢?

这时候引用另外一个api transferToImageBitmap

通过transferToImageBitmap函数可以从OffscreenCanvas对象的绘制内容创建一个ImageBitmap对象。该对象可以用于到其他canvas的绘制。

比如一个常见的使用是,把一个比较耗费时间的绘制放到web worker下的OffscreenCanvas对象上进行,绘制完成后,创建一个ImageBitmap对象,并把该对象传递给页面端,在页面端绘制ImageBitmap对象。

写个小demo测试下:

优化前
我们画 10000 * 10000 个矩形看看页面的响应和时间,代码如下:

const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); function draw() { for(let i = 0;i < 10000;i ){ for(let j = 0;j < 1000;j ){ ctx.fillRect(i*3 j*3 2 2); } } } draw() ctx.arc(100 75 50 0 2*Math.PI); ctx.stroke()

可以很明显的感受到,在渲染出图形前,浏览器是失去响应的,我们无法做认可操作。这样的用户体验肯定是非常差的。

优化后
我们使用离屏canvas webworker 进行优化,代码如下:

我们先看下worker 的代码:

let offscreen ctx; // 监听主线程发的信息 onmessage = function (e) { if(e.data.msg == 'init'){ init(); draw(); } } function init() { offscreen = new OffscreenCanvas(512 512); ctx = offscreen.getContext("2d"); } // 绘制图形 function draw() { ctx.clearRect(0 0 offscreen.width offscreen.height); for(var i = 0;i < 10000;i ){ for(var j = 0;j < 1000;j ){ ctx.fillRect(i*3 j*3 2 2); } } const imageBitmap = offscreen.transferToImageBitmap(); // 传送给主线程 postMessage({imageBitmap:imageBitmap} [imageBitmap]); }

看下主线程的代码:

const worker = new Worker('./worker.js') worker.postMessage({msg:'init'}); worker.onmessage = function (e) { // 这里就接受到work 传来的离屏canvas位图 ctx.drawImage(e.data.imageBitmap 0 0); } ctx.arc(100 75 50 0 2*Math.PI); ctx.stroke()

对比两个很明显的变化, 画多个矩形是个非常耗时的操作会影响其他图形渲染,可以采用离屏canvas webworker 来解决这种失去响应。

5.禁用页面和canvas的滚动事件

touchmove事件和滚动事件有时候是有冲突的 这样在我们移动手指时回导致绘画效果的卡顿 或者事件点位跳跃的情况发生 这时候我们只需要把滚动事件禁用既可以了
禁用方式是在标签上加上或者微信小程序页面加上"disableScroll": true 如果是uniapp在pages.json加上
“disableScroll”: true

<canvas :id="cid" disable-scroll="true" type="2d" ></canvas>

小程序canvas尺寸设置(小程序和H5中canvas卡顿的性能优化方向和实践)(1)

小程序canvas尺寸设置(小程序和H5中canvas卡顿的性能优化方向和实践)(2)

总结
  1. 绘制的图形的数量和大小会影响canvas的性能 减少绘图次数 减少canvas接口调用次数
  2. 图形数量过多,但是只刷新部分 可以使用局部渲染
  3. 逻辑层和背景图层分离 可以使用分层渲染
  4. 某些长时间的逻辑影响主线程的, 可以使用离屏渲染 和webworker 来解决问题
  5. 禁用页面和容器的滚动

猜您喜欢: