零、写在前面
最近在做一些canvas以及WebGL之类的动画,突然发现做粒子动画的时候在数量过多的时候很卡,性能特别低,所以了解了一下性能优化的一些方法,在这里记录一下,也希望得到跟多优化方法的分享以及错误的指正。
这是示例代码以及展示效果,另外可能根据不同设备的本身性能差异效果可能跟下面展示的数据不完全一样,但是大同小异。
一、正常动画实践
为了使用户达到更好的体验,做动画的时候都知道用requestAnimationFrame
了,但是他也是有极限的,当绘制的东西足够多或者复杂的时候,频繁的删除与重绘降低了很多性能。
在canvas中粒子系统应该算是比较常见的一种了,现在创建一个canvas画布,并绘制100个粒子在整个画布上由上至下做匀速往返直线运动。
1.创建一个场景类,并初始化基本数据
class Scene {
constructor(canvas) {
this.canvas = canvas;
this.width = this.canvas.width = 800;
this.height = this.canvas.height = 500;
this.ctx = this.canvas.getContext('2d');
this.amount = 100; // 粒子总数量
this.radius = 5; // 粒子半径
this.particles = []; // 粒子集合
this.speed = 10; // 粒子速度
this.init();
}
}
2.创建一个方法绘制粒子
/* 绘制一个粒子
* ctx —— canvas上下文
* x —— 圆心x坐标
* y —— 圆心y坐标
* r —— 圆半径
*/
drawParticle(ctx, x, y, r) {
ctx.fillStyle = 'black';
ctx.beginPath();
ctx.arc(x, y, r, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
}
3.初始化所有粒子
上面初始化Scene类是调用的
this.init()
。
init() {
this.particles = [];
// 随机位置生成粒子
for (let i = 0; i < this.amount; i++) {
let rx = Math.floor(this.radius + Math.random() * (this.width - this.radius * 2));
let ry = Math.floor(this.radius + Math.random() * (this.height - this.radius * 2));
this.particles.push({
x: rx,
y: ry,
isMax: false // 是否达到边界
});
this.drawParticle(this.ctx, rx, ry, this.radius);
}
// 动画
this.animate();
}
4.运动动画
animate() {
this.ctx.clearRect(0, 0, this.width, this.height);
for (let i = 0; i < this.particles.length; i++) {
let particle = this.particles[i];
// 判断是是否到达边界
if (particle.isMax) {
particle.y -= this.speed;
if (particle.y <= 0 + this.radius) {
particle.isMax = false;
particle.y += this.speed;
}
} else {
particle.y += this.speed;
if (particle.y >= this.height - this.radius) {
particle.isMax = true;
particle.y -= this.speed;
}
}
// 重绘
this.drawParticle(this.ctx, particle.x, particle.y, this.radius);
}
let self = this;
requestAnimationFrame(() => {
self.animate();
});
}
5.分析
从上面最终展示的效果来看100个还是很流畅的,我试着增加数量到1000,发现依然没问题,fps很稳定在60左右,如下图:
1000个粒子
接着增加数量到4000,发现fps已经开始变化了,稳定在50左右,数量越多越明显,如下图。
4000个粒子
二、离屏渲染
1.为什么使用离屏渲染 && 离屏渲染是什么
正如上文所说,当粒子量级达到一定数量的时候,性能开始降低,帧率开始下降,这是我们不想看到的,因为这很影响用户体验。
离屏渲染到底是什么?在Mozilla文档上有简单介绍,大概意思就是说再创建一个新的canvas,然后将要绘制的图形,先在新的canvas中绘制,然后使用drawImage()
将新的canvas画到当前的canvas上。网上看了一些也没有讲的特别清楚,也动手实践了一下。
2.创建粒子(圆形)类
class Particle {
constructor(r) {
this.canvas = document.createElement('canvas'); // 创建一个新的canvas
this.width = this.canvas.width = r * 2; // 创建一个正好包裹住一个粒子canvas
this.height = this.canvas.height = r * 2;
this.ctx = this.canvas.getContext('2d');
this.x = this.width / 2;
this.y = this.height / 2;
this.r = r; // 半径
this.create();
}
// 创建粒子
create() {
this.ctx.save();
this.ctx.fillStyle = 'black';
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
this.ctx.closePath();
this.ctx.fill();
this.ctx.restore();
}
// 移动粒子
move(ctx, x, y) {
// 将这个新创建的canvas画到真正显示的canvas上
ctx.drawImage(this.canvas, x, y);
}
}
3.初始化小球以及动画代码
// init方法中的for循环
for (let i = 0; i < this.amount; i++) {
let rx = Math.floor(this.radius + Math.random() * (this.width - this.radius * 2));
let ry = Math.floor(this.radius + Math.random() * (this.height - this.radius * 2));
// 每一个粒子创建一个实例
let particle = new Particle(this.radius);
this.particles.push({
instance: particle,
x: rx,
y: ry,
isMax: false
});
}
将animate()
方法中原本调用drawParticle()
的地方改为:
// this.drawParticle(this.ctx, particle.x, particle.y, this.radius);
// ↓↓
particle.instance.move(this.ctx, particle.x, particle.y);
4.分析
通过上面的改造,效果如下图:
2000个粒子
但是,发现在2000个粒子的时候fps已经降低到25左右,上面没有使用离屏渲染的时候4000个粒子fps还有50,由此感觉离屏渲染反而降低了性能。
回过头来一想,都说离屏渲染提升性能,难道是操作有问题,接着又进行了一波优化。
三、离屏渲染——代码优化
1.优化思路
在上面相当于创建了跟粒子个数相同的离屏canvas,而且每个都是一样的,既然都是一样的,那干脆就只new一个粒子的实例出来,这样就只创建了一个离屏的canvas,然后通过在不同位置多次绘制这个离屏canvas,实现动画效果。
2.代码修改
修改init
方法,将循环中创建实例的方法放到循环外面。
// 只创建一个实例
let particle = new Particle(this.radius);
for (let i = 0; i < this.amount; i++) {
let rx = Math.floor(this.radius + Math.random() * (this.width - this.radius * 2));
let ry = Math.floor(this.radius + Math.random() * (this.height - this.radius * 2));
this.particles.push({
instance: particle,
x: rx,
y: ry,
isMax: false
});
}
3.分析
优化了代码之后,果然达到了预期的效果,下面是4000个粒子的fps如下图:
四、写在后面
通过上面的操作,发现离屏渲染虽然可以优化动画的性能,但是从上面可以看出频繁的创建和销毁大量canvas也会很影响性能的,所以这中间要有一个取舍。另外,凡事都有一个限度,离屏渲染也不是万能的,有兴趣的可以试试,在这个例子中,如果粒子数量达到7000、8000或者9000乃至更多其实还是有很明显的卡顿。
当然,canvas的一些api也是消耗性能的,所以最后发现,要做好性能优化,首先代码肯定是要优化,另外就是使用像离屏渲染之类的方法。如果有其他优化性能的好方法希望可以分享给我^o^。