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

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

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

什么是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. 禁用页面和容器的滚动

相关推荐

Django零基础速成指南:快速打造带用户系统的博客平台

#python##服务器##API##编程##学习#不是所有教程都值得你花时间!这篇实战指南将用5分钟带你解锁Django核心技能,手把手教你从零搭建一个具备用户注册登录、文章管理功能的完整...

iOS 17.0 Bootstrap 1.2.9 半越狱来啦!更新两点

这款Bootstrap半越狱工具终于更新,离上一次更新已相隔很久,现在推出1.2.9版本,主要为内置两点功能进行更新,也是提升半越狱的稳定性。如果你正在使用这款半越狱工具的,建议你更新。注意!...

iOS 16.x Bootstrap 1.2.3 发布,支持运行清理工具

本文主要讲Bootstrap半越狱工具更新相关内容。如果你是iOS16.0至16.6.1和17.0系统的,想体验半越狱的果粉,请继续往下看。--知识点科普--Bootstrap...

SpringBoot整合工作流引擎Acticiti系统,适用于ERP、OA系统

今日推荐:SpringBoot整合工作流引擎Acticiti的源码推荐理由:1、SpringBoot整合工作流引擎Acticiti系统2、实现了三级权限结构3、持久层使用了mybatis框架4、流程包...

SpringCloud自定义Bootstrap配置指南

在SpringCloud中自定义Bootstrap配置需要以下步骤,以确保在应用启动的早期阶段加载自定义配置:1.添加依赖(针对新版本SpringCloud)从SpringCloud2020...

Python使用Dash开发网页应用(三)(python网页开发教程)

PlotlyDash开发Web应用示例一个好的网页设计通常都需要编写css甚至js来定制前端内容,例如非常流行的bootstrap框架。我们既然想使用Dash来搭建web应用,很大的一个原因是不熟悉...

Oxygen XML Editor 27.1 中的新功能

OxygenXMLEditor27.1版是面向内容作者、开发者、合作者和出版商的行业领先工具包的增量版本。在27.1版本中,AIPositronAssistant得到了增强,包括用于...

【LLM-多模态】Mini-Gemini:挖掘多模态视觉语言模型的潜力

一、结论写在前面论文提出了Mini-Gemini,一个精简而强大的多模态VLM框架。Mini-Gemini的本质在于通过战略性框架设计、丰富的数据质量和扩展的功能范围,发掘VLM的潜在能力。其核心是补...

谐云课堂 | 一文详解分布式改造理论与实战

01微服务与分布式什么是分布式?首先,我们对上图提到的部分关键词进行讲解。单体,是指一个进程完成全部的后端处理;水平拆分,是同一个后端多环境部署,他们都处理相同的内容,使用反向代理来均衡负载,这种也叫...

基于Abaqus的手动挡换挡机构可靠性仿真

手动挡,也称手动变速器,英文全称为Manualtransmission,简称MT,即用手拨动换挡操纵总成才能改变变速器内的齿轮啮合位置,改变传动比,从而达到变速的目的。家用轿车主要采用软轴连接的换挡...

【pytorch】目标检测:彻底搞懂YOLOv5详解

YOLOv5是GlennJocher等人研发,它是Ultralytics公司的开源项目。YOLOv5根据参数量分为了n、s、m、l、x五种类型,其参数量依次上升,当然了其效果也是越来越好。从2020...

超实用!50个非常实用的PS快捷键命令大全分享

今天,给大家介绍50个非常实用的快捷键命令大全,大家伙都是设计师,关于软件使用那是越快越好啊。一、常用的热键组合1、图层混合模式快捷键:正常(Shift+Option+N),正片叠底(Shif...

Pohtoshop中深藏不露的小技巧(科目一考试技巧记忆口诀看完必过)

邢帅教育ps教程为大家总结了一些Pohtoshop中深藏不露的小技巧,可以帮助到大家在设计时减少不必要的麻烦,提高工作效率哦~~~1.设置网格线保持像素完美不在1:1分辨率下也能保持像素完美,可以...

Ganglia监控安装总结(监控安装工作总结)

一、ganglia简介:Ganglia是一个跨平台可扩展的,高性能计算系统下的分布式监控系统,如集群和网格。它是基于分层设计,它使用广泛的技术,如XML数据代表,便携数据传输,RRDtool用于数据...

谁说Adobe XD做不出好看的设计?那是你没搞懂这些功能

AdobeXD的美化栏具有将设计视图美化的功能,它能使界面设计和原型设计更漂亮、更吸引眼球。美化栏的7个功能包括竖线布局设计、横线布局设计、重复网格、图形大小和位置设置、响应式调整大小、文字美化以及...