百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

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

myzbx 2025-04-08 16:45 19 浏览

什么是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变形,这是一个 很常见的需求可能你稍不注意写下了下面这几行代码:

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中,只需要对 动的图形去做重新绘制就好了,其余的依旧是保持不动 。

伪代码

 


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


总结

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

相关推荐

零基础入门AI智能体:详细了解什么是变量类型、JSON结构、Markdown格式

当品牌跳出固有框架,以跨界联动、场景创新叩击年轻群体的兴趣点,一场关于如何在迭代中保持鲜活的探索正在展开,既藏着破圈的巧思,也映照着与新一代对话的密码。在创建AI智能体时,我们会调用插件或大模型,而在...

C# 13模式匹配:递归模式与属性模式在真实代码中的性能影响分析

C#13对模式匹配的增强让复杂数据处理代码更简洁,但递归模式与属性模式的性能差异一直是开发者关注的焦点。在实际项目中,选择合适的模式不仅影响代码可读性,还可能导致执行效率的显著差异。本文结合真实测试...

零基础快速入门 VBA 系列 6 —— 常用对象(工作簿、工作表和区域)

上一节,我介绍了VBA内置函数以及如何自动打字和自动保存文件。这一节,我们来了解一下Excel常用对象。Excel常用对象Excel有很多对象,其中最常用也最重要的包括以下3个:1.Workbo...

不同生命数字的生肖龙!准到雷普!

属龙的人总在自信爆棚和自讨苦吃之间反复横跳?看完这届龙宝宝的日常我悟了。属龙的人好像天生自带矛盾体:领导力超强可人缘时好时坏,工作雷厉风行却总在爱情里翻车。关键年份的龙性格差异更大——76年龙靠谱但不...

仓颉编程语言基础-面向对象编程-属性(Properties)

属性是仓颉颉中一种强大的机制,它允许你封装对类(或接口interface、结构体struct、枚举enum、扩展extend)内部状态的访问。它看起来像一个普通的成员变量(字段),但在其背后,它通过...

Python中class对象/属性/方法/继承/多态/魔法方法详解

一、基础入门:认识类和对象1.类和对象的概念在Python中,类(class)是一种抽象的概念,用于定义对象的属性和行为,而对象(也称为实例)则是类的具体表现。比如,“汽车”可以是一个类,它有...

VBA基础入门:搞清楚对象、属性和方法就成功了一半

如果你刚接触VBA(VisualBasicforApplications),可能会被“对象”“属性”“方法”这些术语搞得一头雾水。但事实上,这三个概念是VBA编程的基石。只要理解它们之间的关系,...

P.O类型文推荐|年度编推合集(一百九十五篇)

点击左上方关注获取更多精彩推文目录2019年度编推35篇(1V1)《悖论》作者:流苏.txt(1V1)《桂花蒸》作者:大姑娘浪.txt(1V1)《豪门浪女》作者:奚行.txt...

Python参数传递内存大揭秘:可变对象 vs 不可变对象

90%的Python程序员不知道,函数参数传递中可变对象的修改竟会导致意想不到的副作用!一、参数传递的本质:对象引用传递在Python中,所有参数传递都是对象引用的传递。这意味着函数调用时传递的不是对...

JS 开发者必看!TC39 2025 最新动向,这些新语法要火?

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。TC39第...

2025 年值得尝试的 5 个被低估的 JavaScript 库

这些JavaScript库可能不会在社交媒体或HackerNews上流行起来,但它们会显著提高您的工作效率和代码质量。JavaScript不再只是框架。虽然React、Vue和Sv...

Python自动化办公应用学习笔记30—函数的参数

一、函数的参数1.形参:o定义:在函数定义时,声明在函数名后面括号中的变量。o作用:它们是函数内部的占位符变量,用于接收函数被调用时传入的实际值。o生命周期:在函数被调用时创建,在函数执...

16种MBTI人格全解析|测完我沉默了三秒:原来我是这样的人?

MBTI性格测试火了这么久,你还不知道自己是哪一型?有人拿它当社交话题,有人拿它分析老板性格,还有人干脆当成择偶参考表。不废话,今天我一次性给你整理全部16种MBTI人格类型!看完你不仅能知道自己是谁...

JS基础与高级应用: 性能优化

在现代Web开发中,性能优化已成为前端工程师必须掌握的核心技能之一。本文从URL输入到页面加载完成的全过程出发,深入分析了HTTP协议的演进、域名解析、代码层面性能优化以及编译与渲染的最佳实践。通过节...

爱思创CSP-J/S初赛模拟赛线上开赛!助力冲入2024年CSP-J/S复赛!

CSP-J/S组初赛模拟赛爱思创,专注信奥教育19年,2022年CSP-J/S组赛事指定考点,特邀NOIP教练,开启全真实CSP-J/S组线上初赛模拟大赛!一、比赛对象:2024年备考CSP-J/S初...