canvas性能优化——离屏渲染

一、正常动画实践

为了使用户达到更好的体验,做动画的时候都知道用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也是消耗性能的,所以最后发现,要做好性能优化,首先代码肯定是要优化,另外就是使用像离屏渲染之类的方法。

转自:https://blog.csdn.net/qq_26733915/article/details/81675124

回过头来一想,都说离屏渲染提升性能,难道是操作有问题,接着又进行了一波优化。4000个粒子

posted @ 2021-02-26 13:20  vickylinj  阅读(1821)  评论(0编辑  收藏  举报