小程序和H5中canvas卡顿的性能优化方向和实践
zhezhongyun 2025-01-24 14:46 47 浏览
什么是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中,只需要对 动的图形去做重新绘制就好了,其余的依旧是保持不动 。
伪代码
<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 来解决问题
- 禁用页面和容器的滚动
相关推荐
- JavaScript中常用数据类型,你知道几个?
-
本文首发自「慕课网」,想了解更多IT干货内容,程序员圈内热闻,欢迎关注!作者|慕课网精英讲师Lison这篇文章我们了解一下JavaScript中现有的八个数据类型,当然这并不是JavaScr...
- 踩坑:前端的z-index 之bug一二(zh1es前端)
-
IE6下浮动元素bug给IE6下的一个div设置元素样式,无论z-index设置多高都不起作用。这种情况发生的条件有三个:1.父标签position属性为relative;2.问题标签无posi...
- 两栏布局、左边定宽200px、右边自适应如何实现?
-
一、两栏布局(左定宽,右自动)1.float+margin即固定宽度元素设置float属性为left,自适应元素设置margin属性,margin-left应>=定宽元素宽度。举例:HTM...
- 前端代码需要这样优化才是一个标准的网站
-
网站由前端和后端组成,前端呈现给用户。本文将告诉您前端页面代码的优化,当然仍然是基于seo优化的。 就前端而言,如果做伪静态处理,基本上是普通的html代码,正常情况下,这些页面内容是通过页面模...
- 网页设计如何自学(初学网页设计)
-
1在Dreamweaver中搭建不同的页面,需要掌握HTML的语句了,通过调整各项数值就可以制作出排版漂亮的页面,跟着就可以学习一些可视化设计软件。下面介绍网页设计如何自学,希望可以帮助到各位。Dre...
- 1、数值类型(数值类型有)
-
1.1数据类型概览MySQL的数据类型可划分为三大类别:数值类型:旨在存储数字(涵盖整型、浮点型、DECIMAL等)。字符串类型:主要用于存储文本(诸如CHAR、VARCHAR之类)。日期/...
- 网页设计的布局属性(网页设计的布局属性是什么)
-
布局属性是网站设计中必不可少的一个重要的环节,主要用来设置网页的元素的布局,主要有以下属性。1、float:该属性设置元素的浮动方式,可以取none,left和right等3个值,分别表示不浮动,浮在...
- Grid网格布局一种更灵活、更强大的二维布局模型!
-
当涉及到网页布局时,display:flex;和display:grid;是两个常用的CSS属性,它们都允许创建不同类型的布局,但有着不同的用法和适用场景。使用flex布局的痛点当我们使...
- React 项目实践——创建一个聊天机器人
-
作者:FredrikStrandOseberg转发链接:https://www.freecodecamp.org/news/how-to-build-a-chatbot-with-react/前言...
- 有趣的 CSS 数学函数(css公式)
-
前言之前一直在玩three.js,接触了很多数学函数,用它们创造过很多特效。于是我思考:能否在CSS中也用上这些数学函数,但发现CSS目前还没有,据说以后的新规范会纳入,估计也要等很久。然...
- web开发之-前端css(5)(css前端设计)
-
显示控制一个元素的显示方式,我们可以使用display:block;display:inline-block;display:none;其中布局相关的还有两个很重要的属性:display:flex;和...
- 2024最新升级–前端内功修炼 5大主流布局系统进阶(分享)
-
获课:keyouit.xyz/14642/1.前端布局的重要性及发展历程前端布局是网页设计和开发的核心技能之一,它决定了页面元素如何组织和呈现。从早期的静态布局到现代的响应式布局,前端布局技术经历了...
- 教你轻松制作自动换行的CSS布局,轻松应对不同设备!
-
在网页设计中,自动换行的CSS布局是非常常见的需求,特别是在响应式设计中。它可以让网页内容自动适应不同屏幕尺寸,保证用户在不同设备上都能够获得良好的浏览体验。本文将介绍几种制作自动换行的CSS布局的方...
- 晨光微语!一道 CSS 面试题,伴你静享知识治愈时光
-
当第一缕阳光温柔地爬上窗台,窗外的鸟鸣声清脆悦耳,空气中弥漫着清新的气息。在这宁静美好的清晨与上午时光,泡一杯热气腾腾的咖啡,找一个舒适的角落坐下。前端的小伙伴们,先把工作的疲惫和面试的焦虑放在一边,...
- 2023 年的响应式设计指南(什么是响应式设计优缺点)
-
大家好,我是Echa。如今,当大家考虑构建流畅的布局时,没有再写固定宽度和高度数值了。相反,小编今天构建的布局需要适用于几乎任何尺寸的设备。是不是不可思议,小编仍然看到网站遵循自适应设计模式,其中它有...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 教程 (33)
- HTML 简介 (35)
- HTML 实例/测验 (32)
- HTML 测验 (32)
- JavaScript 和 HTML DOM 参考手册 (32)
- HTML 拓展阅读 (30)
- HTML常用标签 (29)
- HTML文本框样式 (31)
- HTML滚动条样式 (34)
- HTML5 浏览器支持 (33)
- HTML5 新元素 (33)
- HTML5 WebSocket (30)
- HTML5 代码规范 (32)
- HTML5 标签 (717)
- HTML5 标签 (已废弃) (75)
- HTML5电子书 (32)
- HTML5开发工具 (34)
- HTML5小游戏源码 (34)
- HTML5模板下载 (30)
- HTTP 状态消息 (33)
- HTTP 方法:GET 对比 POST (33)
- 键盘快捷键 (35)
- 标签 (226)
- HTML button formtarget 属性 (30)
- CSS 水平对齐 (Horizontal Align) (30)