小程序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的性能优化。
我们都知道浏览器上渲染动画 每一秒高达60帧,也就是1秒钟内我们完成60次图像绘制, 也就是每一帧图像的绘制时间其实就是(1000/ 60)。 如果在每一帧动画的时间小于 16.7 ms 辣么就会出现卡顿、丢帧。而canvas 其实是一个指令式绘图系统, 他通过绘图指令来完成绘图操作。
影响canvas两个很关键的因素:
第一个渲染的图形数量多,就是调用绘图指令的次数比较多,
第二个渲染的图形大,就是一次绘图渲染的时间比较长
这句话怎么理解呢 , 假设你要在场景中画正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的性能 减少绘图次数 减少canvas接口调用次数
- 图形数量过多,但是只刷新部分 可以使用局部渲染
- 逻辑层和背景图层分离 可以使用分层渲染
- 某些长时间的逻辑影响主线程的, 可以使用离屏渲染 和webworker 来解决问题
- 禁用页面和容器的滚动