談談canvas的性能優化(主要講緩存問題)

  聲明:本文爲原創文章,如需轉載,請註明來源WAxes,謝謝!javascript

  canvas玩多了後,就會自動的要開始考慮性能問題了。怎麼優化canvas的動畫呢?html

  【使用緩存】前端

  使用緩存也就是用離屏canvas進行預渲染了,原理很簡單,就是先繪製到一個離屏canvas中,而後再經過drawImage把離屏canvas畫到主canvas中。可能看到這不少人就會誤解,這不是寫遊戲裏面用的不少的雙緩衝機制麼?java

  其實否則,雙緩衝機制是遊戲編程中爲了防止畫面閃爍,所以會有一個顯示在用戶面前的畫布以及一個後臺畫布,進行繪製時會先將畫面內容繪製到後臺畫布中,再將後臺畫布裏的數據繪製到前臺畫布中。這就是雙緩衝,可是canvas中是沒有雙緩衝的,由於現代瀏覽器基本上都是內置了雙緩衝機制。因此,使用離屏canvas並非雙緩衝,而是把離屏canvas當成一個緩存區。把須要重複繪製的畫面數據進行緩存起來,減小調用canvas的API的消耗。git

  衆所周知,調用canvas的API很消耗性能,因此,當咱們要繪製一些重複的畫面數據時,妥善利用離屏canvas對性能方面有很大的提高,能夠看下下面的DEMOgithub

  1 、 沒使用緩存      web

  2 、 使用了緩存
編程

  能夠看到上面的DEMO的性能不同,下面分析一下緣由:爲了實現每一個圈的樣式,因此繪製圈圈時我用了循環繪製,若是沒用啓用緩存,當頁面的圈圈數量達到必定時,動畫每一幀就要大量調用canvas的API,要進行大量的計算,這樣再好的瀏覽器也會被拖垮啦。canvas

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();

  因此,個人方法很簡單,每一個圈圈對象裏面給他一個離屏canvas做緩存區。數組

  除了建立離屏canvas做爲緩存以外,下面的代碼中有一點很關鍵,就是要設置離屏canvas的寬度和高度,canvas生成後的默認大小是300X150;對於個人代碼中每一個緩存起來圈圈對象半徑最大也就不超過80,因此300X150的大小明顯會形成不少空白區域,會形成資源浪費,因此就要設置一下離屏canvas的寬度和高度,讓它跟緩存起來的元素大小一致,這樣也有利於提升動畫性能。上面的四個demo很明顯的顯示出了性能差距,若是沒有設置寬高,當頁面超過400個圈圈對象時就會卡的不行了,而設置了寬高1000個圈圈對象也不以爲卡。

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();
                }
            }

 

當我實例化圈圈對象時,直接調用緩存方法,把複雜的圈圈直接畫到圈圈對象的離屏canvas中保存起來。

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();
                }

而後在接下來的動畫中,我只須要把圈圈對象的離屏canvas畫到主canvas中,這樣,每一幀調用的canvasAPI就只有這麼一句話:

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

跟以前的for循環繪製比起來,實在是快太多了。因此當須要重複繪製矢量圖的時候或者繪製多個圖片的時候,咱們均可以合理利用離屏canvas來預先把畫面數據緩存起來,在接下來的每一幀中就能減小不少不必的消耗性能的操做。

下面貼出1000個圈圈對象流暢版代碼:

<!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>
View Code

  

  離屏canvas還有一個注意事項,若是你作的效果是會將對象不停地建立和銷燬,請慎重使用離屏canvas,至少不要像我上面寫的那樣給每一個對象的屬性綁定離屏canvas。

  由於若是這樣綁定,當對象被銷燬時,離屏canvas也會被銷燬,而大量的離屏canvas不停地被建立和銷燬,會致使canvas buffer耗費大量GPU資源,容易形成瀏覽器崩潰或者嚴重卡幀現象。解決辦法就是弄一個離屏canvas數組,預先裝進足夠數量的離屏canvas,僅將仍然存活的對象緩存起來,當對象被銷燬時,再解除緩存。這樣就不會致使離屏canvas被銷燬了。

 

 【使用requestAnimationFrame】

  這個就不具體解釋了,估計不少人都知道,這個纔是作動畫的最佳循環,而不是setTimeout或者setInterval。直接貼出兼容性寫法:

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

  

  【避免浮點運算】

  雖然javascript提供了很方便的一些取整方法,像Math.floor,Math.ceil,parseInt,可是,國外友人作過測試,parseInt這個方法作了一些額外的工做(好比檢測數據是否是有效的數值,parseInt 甚至先將參數轉換成了字符串!),因此,直接用parseInt的話相對來講比較消耗性能,那怎樣取整呢,能夠直接用老外寫的很巧妙的方法了:

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

運算符不懂的能夠直接戳:http://www.w3school.com.cn/js/pro_js_operators_bitwise.asp  裏面有詳細解釋

  

  【儘可能減小canvasAPI的調用】

  做粒子效果時,儘可能少使用圓,最好使用方形,由於粒子過小,因此方形看上去也跟圓差很少。至於緣由,很容易理解,咱們畫一個圓須要三個步驟:先beginPath,而後用arc畫弧,再用fill進行填充才能產生一個圓。可是畫方形,只須要一個fillRect就能夠了。雖然只是差了兩個調用,當粒子對象數量達到必定時,這性能差距就會顯示出來了。

  還有一些其餘注意事項,我就不一一列舉了,由於谷歌上一搜也挺多的。我這也算是一個給本身作下記錄,主要是記錄緩存的用法。想要提高canvas的性能最主要的仍是得注意代碼的結構,減小沒必要要的API調用,在每一幀中減小複雜的運算或者把複雜運算由每一幀算一次改爲數幀算一次。同時,上面所述的緩存用法,我由於貪圖方便,因此是每一個對象一個離屏canvas,其實離屏canvas也不能用的太氾濫,若是用太多離屏canvas也會有性能問題,請儘可能合理利用離屏canvas。

   源碼地址:https://github.com/whxaxes/canvas-test/tree/gh-pages/src/Other-demo/cache

  本人前端小菜,寫的很差請見諒。

相關文章
相關標籤/搜索