canvas玩多了后,就會(huì)自動(dòng)的要開始考慮性能問題了。怎么優(yōu)化canvas的動(dòng)畫呢?
【使用緩存】
使用緩存也就是用離屏canvas進(jìn)行預(yù)渲染了,原理很簡(jiǎn)單,就是先繪制到一個(gè)離屏canvas中,然后再通過drawImage把離屏canvas畫到主canvas中。可能看到這很多人就會(huì)誤解,這不是寫游戲里面用的很多的雙緩沖機(jī)制么?
其實(shí)不然,雙緩沖機(jī)制是游戲編程中為了防止畫面閃爍,因此會(huì)有一個(gè)顯示在用戶面前的畫布以及一個(gè)后臺(tái)畫布,進(jìn)行繪制時(shí)會(huì)先將畫面內(nèi)容繪制到后臺(tái)畫布中,再將后臺(tái)畫布里的數(shù)據(jù)繪制到前臺(tái)畫布中。這就是雙緩沖,但是canvas中是沒有雙緩沖的,因?yàn)楝F(xiàn)代瀏覽器基本上都是內(nèi)置了雙緩沖機(jī)制。所以,使用離屏canvas并不是雙緩沖,而是把離屏canvas當(dāng)成一個(gè)緩存區(qū)。把需要重復(fù)繪制的畫面數(shù)據(jù)進(jìn)行緩存起來,減少調(diào)用canvas的API的消耗。
眾所周知,調(diào)用canvas的API很消耗性能,所以,當(dāng)我們要繪制一些重復(fù)的畫面數(shù)據(jù)時(shí),妥善利用離屏canvas對(duì)性能方面有很大的提升,可以看下下面的DEMO
1 、 沒使用緩存
2、 使用了緩存但是沒有設(shè)置離屏canvas的寬高
3 、 使用了緩存但是沒有設(shè)置離屏canvas的寬高
4 、 使用了緩存且設(shè)置了離屏canvas的寬高
可以看到上面的DEMO的性能不一樣,下面分析一下原因:為了實(shí)現(xiàn)每個(gè)圈的樣式,所以繪制圈圈時(shí)我用了循環(huán)繪制,如果沒用啟用緩存,當(dāng)頁面的圈圈數(shù)量達(dá)到一定時(shí),動(dòng)畫每一幀就要大量調(diào)用canvas的API,要進(jìn)行大量的計(jì)算,這樣再好的瀏覽器也會(huì)被拖垮啦。
XML/HTML Code復(fù)制內(nèi)容到剪貼板
- 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();
所以,我的方法很簡(jiǎn)單,每個(gè)圈圈對(duì)象里面給他一個(gè)離屏canvas作緩存區(qū)。
除了創(chuàng)建離屏canvas作為緩存之外,下面的代碼中有一點(diǎn)很關(guān)鍵,就是要設(shè)置離屏canvas的寬度和高度,canvas生成后的默認(rèn)大小是300X150;對(duì)于我的代碼中每個(gè)緩存起來圈圈對(duì)象半徑最大也就不超過80,所以300X150的大小明顯會(huì)造成很多空白區(qū)域,會(huì)造成資源浪費(fèi),所以就要設(shè)置一下離屏canvas的寬度和高度,讓它跟緩存起來的元素大小一致,這樣也有利于提高動(dòng)畫性能。上面的四個(gè)demo很明顯的顯示出了性能差距,如果沒有設(shè)置寬高,當(dāng)頁面超過400個(gè)圈圈對(duì)象時(shí)就會(huì)卡的不行了,而設(shè)置了寬高1000個(gè)圈圈對(duì)象也不覺得卡。
XML/HTML Code復(fù)制內(nèi)容到剪貼板
- 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");
- thisthis.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();
- }
- }
當(dāng)我實(shí)例化圈圈對(duì)象時(shí),直接調(diào)用緩存方法,把復(fù)雜的圈圈直接畫到圈圈對(duì)象的離屏canvas中保存起來。
XML/HTML Code復(fù)制內(nèi)容到剪貼板
- cache:function(){
- this.cacheCtx.save();
- var j=0;
- this.cacheCtx.lineWidth = borderWidth;
- for(var i=1;i<this.r;i+=borderWidth){
- this.cacheCtx.beginPath();
- thisthis.cacheCtx.strokeStyle = this.color[j];
- this.cacheCtx.arc(this.r , this.r , i , 0 , 2*Math.PI);
- this.cacheCtx.stroke();
- j++;
- }
- this.cacheCtx.restore();
- }
然后在接下來的動(dòng)畫中,我只需要把圈圈對(duì)象的離屏canvas畫到主canvas中,這樣,每一幀調(diào)用的canvasAPI就只有這么一句話:
XML/HTML Code復(fù)制內(nèi)容到剪貼板
- ctx.drawImage(this.cacheCanvas , this.x-this.r , this.y-this.r);
跟之前的for循環(huán)繪制比起來,實(shí)在是快太多了。所以當(dāng)需要重復(fù)繪制矢量圖的時(shí)候或者繪制多個(gè)圖片的時(shí)候,我們都可以合理利用離屏canvas來預(yù)先把畫面數(shù)據(jù)緩存起來,在接下來的每一幀中就能減少很多沒必要的消耗性能的操作。
下面貼出1000個(gè)圈圈對(duì)象流暢版代碼:
XML/HTML Code復(fù)制內(nèi)容到剪貼板
- <!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>測(cè)試</title>
- </head>
- <body>
- <div >
- <canvas id='cas' width="800" height="600">瀏覽器不支持canvas</canvas>
- <div style="text-align:center">1000個(gè)圈圈對(duì)象也不卡</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");
- thisthis.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();
- thisthis.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){
- thisthis.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){
- thisthis.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>
離屏canvas還有一個(gè)注意事項(xiàng),如果你做的效果是會(huì)將對(duì)象不停地創(chuàng)建和銷毀,請(qǐng)慎重使用離屏canvas,至少不要像我上面寫的那樣給每個(gè)對(duì)象的屬性綁定離屏canvas。
因?yàn)槿绻@樣綁定,當(dāng)對(duì)象被銷毀時(shí),離屏canvas也會(huì)被銷毀,而大量的離屏canvas不停地被創(chuàng)建和銷毀,會(huì)導(dǎo)致canvas buffer耗費(fèi)大量GPU資源,容易造成瀏覽器崩潰或者嚴(yán)重卡幀現(xiàn)象。解決辦法就是弄一個(gè)離屏canvas數(shù)組,預(yù)先裝進(jìn)足夠數(shù)量的離屏canvas,僅將仍然存活的對(duì)象緩存起來,當(dāng)對(duì)象被銷毀時(shí),再解除緩存。這樣就不會(huì)導(dǎo)致離屏canvas被銷毀了。
【使用requestAnimationFrame】
這個(gè)就不具體解釋了,估計(jì)很多人都知道,這個(gè)才是做動(dòng)畫的最佳循環(huán),而不是setTimeout或者setInterval。直接貼出兼容性寫法:
XML/HTML Code復(fù)制內(nèi)容到剪貼板
- window.RAF = (function(){
- return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) {window.setTimeout(callback, 1000 / 60); };
- })();
【避免浮點(diǎn)運(yùn)算】
雖然javascript提供了很方便的一些取整方法,像Math.floor,Math.ceil,parseInt,但是,國外友人做過測(cè)試,parseInt這個(gè)方法做了一些額外的工作(比如檢測(cè)數(shù)據(jù)是不是有效的數(shù)值,parseInt 甚至先將參數(shù)轉(zhuǎn)換成了字符串!),所以,直接用parseInt的話相對(duì)來說比較消耗性能,那怎樣取整呢,可以直接用老外寫的很巧妙的方法了:
JavaScript Code復(fù)制內(nèi)容到剪貼板
1.rounded = (0.5 + somenum) | 0;
2.rounded = ~~ (0.5 + somenum); 3.rounded = (0.5 + somenum) << 0;
運(yùn)算符不懂的可以直接戳:http://www.w3school.com.cn/js/pro_js_operators_bitwise.asp 里面有詳細(xì)解釋
【盡量減少canvasAPI的調(diào)用】
作粒子效果時(shí),盡量少使用圓,最好使用方形,因?yàn)榱W犹?,所以方形看上去也跟圓差不多。至于原因,很容易理解,我們畫一個(gè)圓需要三個(gè)步驟:先beginPath,然后用arc畫弧,再用fill進(jìn)行填充才能產(chǎn)生一個(gè)圓。但是畫方形,只需要一個(gè)fillRect就可以了。雖然只是差了兩個(gè)調(diào)用,當(dāng)粒子對(duì)象數(shù)量達(dá)到一定時(shí),這性能差距就會(huì)顯示出來了。
還有一些其他注意事項(xiàng),我就不一一列舉了,因?yàn)楣雀枭弦凰岩餐Χ嗟?。我這也算是一個(gè)給自己做下記錄,主要是記錄緩存的用法。想要提升canvas的性能最主要的還是得注意代碼的結(jié)構(gòu),減少不必要的API調(diào)用,在每一幀中減少復(fù)雜的運(yùn)算或者把復(fù)雜運(yùn)算由每一幀算一次改成數(shù)幀算一次。同時(shí),上面所述的緩存用法,我因?yàn)樨潏D方便,所以是每個(gè)對(duì)象一個(gè)離屏canvas,其實(shí)離屏canvas也不能用的太泛濫,如果用太多離屏canvas也會(huì)有性能問題,請(qǐng)盡量合理利用離屏canvas。
源碼地址:https://github.com/whxaxes/canvas-test/tree/gh-pages/src/Other-demo/cache