Talk about the performance optimization of canvas, mainly about caching

After playing with canvas a lot, you will automatically start to consider performance issues. How to optimize the animation of canvas? 

use cache

  Using cache means pre-rendering with off-screen canvas. The principle is very simple, that is, draw to an off-screen canvas first, and then draw the off-screen canvas to the main canvas through drawImage. Many people may misunderstand this when they see this. Isn’t this the double buffering mechanism used in many games?

  In fact, the double buffering mechanism is to prevent the screen from flickering in game programming, so there will be a canvas displayed in front of the user and a background canvas. When drawing, the content of the screen will be drawn into the background canvas first, and then the background canvas The data is drawn to the foreground canvas. This is double buffering, but there is no double buffering in canvas, because modern browsers basically have a built-in double buffering mechanism. Therefore, using off-screen canvas is not double buffering, but treating off-screen canvas as a buffer. Cache the screen data that needs to be drawn repeatedly to reduce the consumption of calling the canvas API.

  As we all know, calling the canvas API consumes a lot of performance. Therefore, when we want to draw some repeated screen data, proper use of off-screen canvas can greatly improve performance. You can see the following DEMO

  1.  No cache is used      

  2,  using the cache

  You can see that the performance of the above DEMO is different. Let’s analyze the reason below: In order to realize the style of each circle, I used loop drawing when drawing circles. If it is not used to enable caching, when the number of circles on the page reaches a certain , Each frame of the animation requires a large number of calls to canvas APIs, and a large number of calculations are required, so that even the best browsers will be dragged down.

ctx.save();
                        var j=0;
                        ctx.lineWidth = borderWidth;
                        for(var i=1;i<this.r;i+=borderWidth){
                            ctx.beginPath();
                            ctx.strokeStyle = this.color[j];
                            ctx.arc(this.x , this.y , i , 0 , 2*Math.PI);
                            ctx.stroke();
                            j++;
                        }
                        ctx.restore();

  Therefore, my method is very simple. Each circle object is given an off-screen canvas as a buffer.

  In addition to creating an off-screen canvas as a cache, one of the key points in the following code is to set the width and height of the off-screen canvas. The default size of the canvas after generation is 300X150; for each cache in my code, circle The maximum object radius does not exceed 80, so the size of 300X150 will obviously cause a lot of blank space, which will cause waste of resources, so it is necessary to set the width and height of the off-screen canvas to make it the same size as the cached elements, so that Helps improve animation performance. The above four demos clearly show the performance gap. If the width and height are not set, when the page exceeds 400 circle objects, it will be stuck. However, if the width and height of 1000 circle objects are set, it will not feel stuck. .

var ball = function(x , y , vx , vy , useCache){
                this.x = x;
                this.y = y;
                this.vx = vx;
                this.vy = vy;
                this.r = getZ(getRandom(20,40));
                this.color = [];
                this.cacheCanvas = document.createElement("canvas");
                this.cacheCtx = this.cacheCanvas.getContext("2d");
                this.cacheCanvas.width = 2*this.r;
                this.cacheCanvas.height = 2*this.r;
                var num = getZ(this.r/borderWidth);
                for(var j=0;j<num;j++){
                    this.color.push("rgba("+getZ(getRandom(0,255))+","+getZ(getRandom(0,255))+","+getZ(getRandom(0,255))+",1)");
                }
                this.useCache = useCache;
                if(useCache){
                    this.cache();
                }
            }

When I instantiate the circle object, I directly call the cache method to draw the complex circle directly to the off-screen canvas of the circle object and save it.

cache:function(){
                    this.cacheCtx.save();
                    var j=0;
                    this.cacheCtx.lineWidth = borderWidth;
                    for(var i=1;i<this.r;i+=borderWidth){
                        this.cacheCtx.beginPath();
                        this.cacheCtx.strokeStyle = this.color[j];
                        this.cacheCtx.arc(this.r , this.r , i , 0 , 2*Math.PI);
                        this.cacheCtx.stroke();
                        j++;
                    }
                    this.cacheCtx.restore();
                }

Then in the next animation, I only need to draw the off-screen canvas of the circle object to the main canvas, so that the canvasAPI called in each frame only has this sentence:

ctx.drawImage(this.cacheCanvas , this.x-this.r , this.y-this.r);

Compared with the previous for loop drawing, it is much faster. So when we need to repeatedly draw vector graphics or draw multiple pictures, we can make reasonable use of off-screen canvas to pre-cache the picture data, which can reduce a lot of unnecessary performance consumption in the next frame operation.

The smooth version code of 1000 circle objects is posted below:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <style>
        body{
            padding:0;
            margin:0;
            overflow: hidden;
        }
        #cas{
            display: block;
            background-color:rgba(0,0,0,0);
            margin:auto;
            border:1px solid;
        }
    </style>
    <title>测试</title>
</head>
<body>
    <div >
        <canvas id='cas' width="800" height="600">浏览器不支持canvas</canvas>
        <div style="text-align:center">1000个圈圈对象也不卡</div>
    </div>

    <script>
        var testBox = function(){
            var canvas = document.getElementById("cas"),
                ctx = canvas.getContext('2d'),
                borderWidth = 2,
                Balls = [];
            var ball = function(x , y , vx , vy , useCache){
                this.x = x;
                this.y = y;
                this.vx = vx;
                this.vy = vy;
                this.r = getZ(getRandom(20,40));
                this.color = [];
                this.cacheCanvas = document.createElement("canvas");
                this.cacheCtx = this.cacheCanvas.getContext("2d");
                this.cacheCanvas.width = 2*this.r;
                this.cacheCanvas.height = 2*this.r;
                var num = getZ(this.r/borderWidth);
                for(var j=0;j<num;j++){
                    this.color.push("rgba("+getZ(getRandom(0,255))+","+getZ(getRandom(0,255))+","+getZ(getRandom(0,255))+",1)");
                }
                this.useCache = useCache;
                if(useCache){
                    this.cache();
                }
            }

            function getZ(num){
                var rounded;
                rounded = (0.5 + num) | 0;
                // A double bitwise not.
                rounded = ~~ (0.5 + num);
                // Finally, a left bitwise shift.
                rounded = (0.5 + num) << 0;

                return rounded;
            }

            ball.prototype = {
                paint:function(ctx){
                    if(!this.useCache){
                        ctx.save();
                        var j=0;
                        ctx.lineWidth = borderWidth;
                        for(var i=1;i<this.r;i+=borderWidth){
                            ctx.beginPath();
                            ctx.strokeStyle = this.color[j];
                            ctx.arc(this.x , this.y , i , 0 , 2*Math.PI);
                            ctx.stroke();
                            j++;
                        }
                        ctx.restore();
                    } else{
                        ctx.drawImage(this.cacheCanvas , this.x-this.r , this.y-this.r);
                    }
                },

                cache:function(){
                    this.cacheCtx.save();
                    var j=0;
                    this.cacheCtx.lineWidth = borderWidth;
                    for(var i=1;i<this.r;i+=borderWidth){
                        this.cacheCtx.beginPath();
                        this.cacheCtx.strokeStyle = this.color[j];
                        this.cacheCtx.arc(this.r , this.r , i , 0 , 2*Math.PI);
                        this.cacheCtx.stroke();
                        j++;
                    }
                    this.cacheCtx.restore();
                },

                move:function(){
                    this.x += this.vx;
                    this.y += this.vy;
                    if(this.x>(canvas.width-this.r)||this.x<this.r){
                        this.x=this.x<this.r?this.r:(canvas.width-this.r);
                        this.vx = -this.vx;
                    }
                    if(this.y>(canvas.height-this.r)||this.y<this.r){
                        this.y=this.y<this.r?this.r:(canvas.height-this.r);
                        this.vy = -this.vy;
                    }

                    this.paint(ctx);
                }
            }

            var Game = {
                init:function(){
                    for(var i=0;i<1000;i++){
                        var b = new ball(getRandom(0,canvas.width) , getRandom(0,canvas.height) , getRandom(-10 , 10) ,  getRandom(-10 , 10) , true)
                        Balls.push(b);
                    }
                },

                update:function(){
                    ctx.clearRect(0,0,canvas.width,canvas.height);
                    for(var i=0;i<Balls.length;i++){
                        Balls[i].move();
                    }
                },

                loop:function(){
                    var _this = this;
                    this.update();
                    RAF(function(){
                        _this.loop();
                    })
                },

                start:function(){
                    this.init();
                    this.loop();
                }
            }

            window.RAF = (function(){
                return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) {window.setTimeout(callback, 1000 / 60); };
            })();

            return Game;
        }();

        function getRandom(a , b){
            return Math.random()*(b-a)+a;
        }

        window.onload = function(){
            testBox.start();
        }
    </script>
</body>
</html>

  There is another note about off-screen canvas. If the effect you make is to create and destroy objects continuously, please use off-screen canvas carefully. At least don’t bind the properties of each object to off-screen like I wrote above. canvas.

  Because if bound in this way, when the object is destroyed, the off-screen canvas will also be destroyed, and a large number of off-screen canvases will be created and destroyed continuously, which will cause the canvas buffer to consume a lot of GPU resources, which will easily cause the browser to crash or seriously Frame stuck phenomenon. The solution is to make an array of off-screen canvases, preload enough off-screen canvases, only cache the surviving objects, and uncache them when the objects are destroyed. This will not cause the off-screen canvas to be destroyed.

Use requestAnimationFrame

  I won’t explain this in detail. It is estimated that many people know that this is the best cycle for animation, not setTimeout or setInterval. Directly post compatibility writing:

window.RAF = (function(){
       return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) {window.setTimeout(callback, 1000 / 60); };
            })();

Avoid floating point operations

  Although javascript provides some convenient rounding methods, such as Math. parseInt even converts the parameter into a string first!), so, using parseInt directly is relatively cost-effective, so how to round it up, you can directly use a very clever method written by foreigners:

rounded = (0.5 + somenum) | 0;
rounded = ~~ (0.5 + somenum);
rounded = (0.5 + somenum) << 0;

If you don’t understand the operator, you can poke it directly:   There is a detailed explanation in ECMAScript bitwise operator

  

Minimize canvas API calls

  When making particle effects, try to use circles as little as possible, and it is better to use squares, because the particles are too small, so squares look similar to circles. As for the reason, it is easy to understand that we need three steps to draw a circle: first beginPath, then use arc to draw an arc, and then fill to generate a circle. But to draw a square, only one fillRect is needed. Although there is only a difference of two calls, when the number of particle objects reaches a certain level, the performance gap will be displayed.

  There are some other precautions, I will not list them one by one, because there are quite a lot of searches on Google. This can be regarded as a record for myself, mainly to record the usage of the cache. The most important thing to improve the performance of canvas is to pay attention to the structure of the code, reduce unnecessary API calls, reduce complex operations in each frame or change complex operations from one calculation per frame to one calculation per frame. At the same time, for the cache usage mentioned above, because I am greedy for convenience, I use an off-screen canvas for each object. In fact, the off-screen canvas cannot be used too much. If you use too many off-screen canvases, there will be performance problems. Please try your best. Reasonable use of off-screen canvas.

Guess you like

Origin blog.csdn.net/weixin_44786530/article/details/130501034