一个分享WordPress、Zblog、Emlog、Typecho等主流博客的教程网站!
当前位置:网站首页 > 其他相关教程 > 正文

小程序和H5中canvas卡顿的性能优化方向和实践

作者:xlnxin发布时间:2023-08-29分类:其他相关教程浏览:1562


导读:什么是CANVAS?首先介绍下canvas,前端的同学可能很熟悉,举个很简单的例子,平常用的网页截图、H5游戏、前端动效、可视化图表…,都有canvas的应用场景,官方的定...

什么是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两个很关键的因素:
第一个渲染的图形数量多,就是调用绘图指令的次数比较多,
第二个渲染的图形大,就是一次绘图渲染的时间比较长

优化canvas

1. 减少绘图指令的调用

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

Bash
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 对应的生成多边形的点,代码如下:

Bash
 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>



总结

  1. 绘制的图形的数量和大小会影响canvas的性能,减少绘图次数,减少canvas接口调用次数

  2. 图形数量过多,但是只刷新部分 可以使用局部渲染

  3. 逻辑层和背景图层分离 可以使用分层渲染

  4. 某些长时间的逻辑影响主线程的, 可以使用离屏渲染 和webworker 来解决问题

  5. 禁用页面和容器的滚动


标签:空间程序htmlhtml5小程序