浅谈利用缓存来优化HTML5 Canvas程序的性能


Posted in HTML / CSS onMay 12, 2015

canvas玩多了后,就会自动的要开始考虑性能问题了。怎么优化canvas的动画呢?

【使用缓存】

使用缓存也就是用离屏canvas进行预渲染了,原理很简单,就是先绘制到一个离屏canvas中,然后再通过drawImage把离屏canvas画到主canvas中。可能看到这很多人就会误解,这不是写游戏里面用的很多的双缓冲机制么?

其实不然,双缓冲机制是游戏编程中为了防止画面闪烁,因此会有一个显示在用户面前的画布以及一个后台画布,进行绘制时会先将画面内容绘制到后台画布中,再将后台画布里的数据绘制到前台画布中。这就是双缓冲,但是canvas中是没有双缓冲的,因为现代浏览器基本上都是内置了双缓冲机制。所以,使用离屏canvas并不是双缓冲,而是把离屏canvas当成一个缓存区。把需要重复绘制的画面数据进行缓存起来,减少调用canvas的API的消耗。

众所周知,调用canvas的API很消耗性能,所以,当我们要绘制一些重复的画面数据时,妥善利用离屏canvas对性能方面有很大的提升,可以看下下面的DEMO

1 、 没使用缓存   

2、  使用了缓存但是没有设置离屏canvas的宽高  

      3 、 使用了缓存但是没有设置离屏canvas的宽高  

4 、 使用了缓存且设置了离屏canvas的宽高

可以看到上面的DEMO的性能不一样,下面分析一下原因:为了实现每个圈的样式,所以绘制圈圈时我用了循环绘制,如果没用启用缓存,当页面的圈圈数量达到一定时,动画每一帧就要大量调用canvas的API,要进行大量的计算,这样再好的浏览器也会被拖垮啦。
XML/HTML Code复制内容到剪贴板

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

所以,我的方法很简单,每个圈圈对象里面给他一个离屏canvas作缓存区。

除了创建离屏canvas作为缓存之外,下面的代码中有一点很关键,就是要设置离屏canvas的宽度和高度,canvas生成后的默认大小是300X150;对于我的代码中每个缓存起来圈圈对象半径最大也就不超过80,所以300X150的大小明显会造成很多空白区域,会造成资源浪费,所以就要设置一下离屏canvas的宽度和高度,让它跟缓存起来的元素大小一致,这样也有利于提高动画性能。上面的四个demo很明显的显示出了性能差距,如果没有设置宽高,当页面超过400个圈圈对象时就会卡的不行了,而设置了宽高1000个圈圈对象也不觉得卡。

XML/HTML Code复制内容到剪贴板
  1. var ball = function(x , y , vx , vy , useCache){   
  2.                 this.x = x;   
  3.                 this.y = y;   
  4.                 this.vx = vx;   
  5.                 this.vy = vy;   
  6.                 this.r = getZ(getRandom(20,40));   
  7.                 this.color = [];   
  8.                 this.cacheCanvas = document.createElement("canvas");   
  9.                 thisthis.cacheCtx = this.cacheCanvas.getContext("2d");   
  10.                 this.cacheCanvas.width = 2*this.r;   
  11.                 this.cacheCanvas.height = 2*this.r;   
  12.                 var num = getZ(this.r/borderWidth);   
  13.                 for(var j=0;j<num;j++){   
  14.                     this.color.push("rgba("+getZ(getRandom(0,255))+","+getZ(getRandom(0,255))+","+getZ(getRandom(0,255))+",1)");   
  15.                 }   
  16.                 this.useCache = useCache;   
  17.                 if(useCache){   
  18.                     this.cache();   
  19.                 }   
  20.             }  

 

当我实例化圈圈对象时,直接调用缓存方法,把复杂的圈圈直接画到圈圈对象的离屏canvas中保存起来。

XML/HTML Code复制内容到剪贴板
  1. cache:function(){   
  2.                     this.cacheCtx.save();   
  3.                     var j=0;   
  4.                     this.cacheCtx.lineWidth = borderWidth;   
  5.                     for(var i=1;i<this.r;i+=borderWidth){   
  6.                         this.cacheCtx.beginPath();   
  7.                         thisthis.cacheCtx.strokeStyle = this.color[j];   
  8.                         this.cacheCtx.arc(this.r , this.r , i , 0 , 2*Math.PI);   
  9.                         this.cacheCtx.stroke();   
  10.                         j++;   
  11.                     }   
  12.                     this.cacheCtx.restore();   
  13.                 }  

然后在接下来的动画中,我只需要把圈圈对象的离屏canvas画到主canvas中,这样,每一帧调用的canvasAPI就只有这么一句话:

XML/HTML Code复制内容到剪贴板
  1. ctx.drawImage(this.cacheCanvas , this.x-this.r , this.y-this.r);  

跟之前的for循环绘制比起来,实在是快太多了。所以当需要重复绘制矢量图的时候或者绘制多个图片的时候,我们都可以合理利用离屏canvas来预先把画面数据缓存起来,在接下来的每一帧中就能减少很多没必要的消耗性能的操作。

下面贴出1000个圈圈对象流畅版代码:

XML/HTML Code复制内容到剪贴板
  1. <!doctype html>  
  2. <html lang="en">  
  3. <head>  
  4.     <meta charset="UTF-8">  
  5.     <style>  
  6.         body{   
  7.             padding:0;   
  8.             margin:0;   
  9.             overflow: hidden;   
  10.         }   
  11.         #cas{   
  12.             display: block;   
  13.             background-color:rgba(0,0,0,0);   
  14.             margin:auto;   
  15.             border:1px solid;   
  16.         }   
  17.     </style>  
  18.     <title>测试</title>  
  19. </head>  
  20. <body>  
  21.     <div >  
  22.         <canvas id='cas' width="800" height="600">浏览器不支持canvas</canvas>  
  23.         <div style="text-align:center">1000个圈圈对象也不卡</div>  
  24.     </div>  
  25.   
  26.     <script>  
  27.         var testBox = function(){   
  28.             var canvas = document.getElementById("cas"),   
  29.                 ctx = canvas.getContext('2d'),   
  30.                 borderWidth = 2,   
  31.                 Balls = [];   
  32.             var ball = function(x , y , vx , vy , useCache){   
  33.                 this.x = x;   
  34.                 this.y = y;   
  35.                 this.vx = vx;   
  36.                 this.vy = vy;   
  37.                 this.r = getZ(getRandom(20,40));   
  38.                 this.color = [];   
  39.                 this.cacheCanvas = document.createElement("canvas");   
  40.                 thisthis.cacheCtx = this.cacheCanvas.getContext("2d");   
  41.                 this.cacheCanvas.width = 2*this.r;   
  42.                 this.cacheCanvas.height = 2*this.r;   
  43.                 var num = getZ(this.r/borderWidth);   
  44.                 for(var j=0;j<num;j++){   
  45.                     this.color.push("rgba("+getZ(getRandom(0,255))+","+getZ(getRandom(0,255))+","+getZ(getRandom(0,255))+",1)");   
  46.                 }   
  47.                 this.useCache = useCache;   
  48.                 if(useCache){   
  49.                     this.cache();   
  50.                 }   
  51.             }   
  52.   
  53.             function getZ(num){   
  54.                 var rounded;   
  55.                 rounded = (0.5 + num) | 0;   
  56.                 // A double bitwise not.   
  57.                 rounded = ~~ (0.5 + num);   
  58.                 // Finally, a left bitwise shift.   
  59.                 rounded = (0.5 + num) << 0;   
  60.   
  61.                 return rounded;   
  62.             }   
  63.   
  64.             ball.prototype = {   
  65.                 paint:function(ctx){   
  66.                     if(!this.useCache){   
  67.                         ctx.save();   
  68.                         var j=0;   
  69.                         ctx.lineWidth = borderWidth;   
  70.                         for(var i=1;i<this.r;i+=borderWidth){   
  71.                             ctx.beginPath();   
  72.                             ctx.strokeStyle = this.color[j];   
  73.                             ctx.arc(this.x , this.y , i , 0 , 2*Math.PI);   
  74.                             ctx.stroke();   
  75.                             j++;   
  76.                         }   
  77.                         ctx.restore();   
  78.                     } else{   
  79.                         ctx.drawImage(this.cacheCanvas , this.x-this.r , this.y-this.r);   
  80.                     }   
  81.                 },   
  82.   
  83.                 cache:function(){   
  84.                     this.cacheCtx.save();   
  85.                     var j=0;   
  86.                     this.cacheCtx.lineWidth = borderWidth;   
  87.                     for(var i=1;i<this.r;i+=borderWidth){   
  88.                         this.cacheCtx.beginPath();   
  89.                         thisthis.cacheCtx.strokeStyle = this.color[j];   
  90.                         this.cacheCtx.arc(this.r , this.r , i , 0 , 2*Math.PI);   
  91.                         this.cacheCtx.stroke();   
  92.                         j++;   
  93.                     }   
  94.                     this.cacheCtx.restore();   
  95.                 },   
  96.   
  97.                 move:function(){   
  98.                     this.x += this.vx;   
  99.                     this.y += this.vy;   
  100.                     if(this.x>(canvas.width-this.r)||this.x<this.r){   
  101.                         thisthis.x=this.x<this.r?this.r:(canvas.width-this.r);   
  102.                         this.vx = -this.vx;   
  103.                     }   
  104.                     if(this.y>(canvas.height-this.r)||this.y<this.r){   
  105.                         thisthis.y=this.y<this.r?this.r:(canvas.height-this.r);   
  106.                         this.vy = -this.vy;   
  107.                     }   
  108.   
  109.                     this.paint(ctx);   
  110.                 }   
  111.             }   
  112.   
  113.             var Game = {   
  114.                 init:function(){   
  115.                     for(var i=0;i<1000;i++){   
  116.                         var b = new ball(getRandom(0,canvas.width) , getRandom(0,canvas.height) , getRandom(-10 , 10) ,  getRandom(-10 , 10) , true)   
  117.                         Balls.push(b);   
  118.                     }   
  119.                 },   
  120.   
  121.                 update:function(){   
  122.                     ctx.clearRect(0,0,canvas.width,canvas.height);   
  123.                     for(var i=0;i<Balls.length;i++){   
  124.                         Balls[i].move();   
  125.                     }   
  126.                 },   
  127.   
  128.                 loop:function(){   
  129.                     var _this = this;   
  130.                     this.update();   
  131.                     RAF(function(){   
  132.                         _this.loop();   
  133.                     })   
  134.                 },   
  135.   
  136.                 start:function(){   
  137.                     this.init();   
  138.                     this.loop();   
  139.                 }   
  140.             }   
  141.   
  142.             window.RAF = (function(){   
  143.                 return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) {window.setTimeout(callback, 1000 / 60); };   
  144.             })();   
  145.   
  146.             return Game;   
  147.         }();   
  148.   
  149.         function getRandom(a , b){   
  150.             return Math.random()*(b-a)+a;   
  151.         }   
  152.   
  153.         window.onload = function(){   
  154.             testBox.start();   
  155.         }   
  156.     </script>  
  157. </body>  
  158. </html>  

离屏canvas还有一个注意事项,如果你做的效果是会将对象不停地创建和销毁,请慎重使用离屏canvas,至少不要像我上面写的那样给每个对象的属性绑定离屏canvas。

因为如果这样绑定,当对象被销毁时,离屏canvas也会被销毁,而大量的离屏canvas不停地被创建和销毁,会导致canvas buffer耗费大量GPU资源,容易造成浏览器崩溃或者严重卡帧现象。解决办法就是弄一个离屏canvas数组,预先装进足够数量的离屏canvas,仅将仍然存活的对象缓存起来,当对象被销毁时,再解除缓存。这样就不会导致离屏canvas被销毁了。

 

 【使用requestAnimationFrame】

这个就不具体解释了,估计很多人都知道,这个才是做动画的最佳循环,而不是setTimeout或者setInterval。直接贴出兼容性写法:

XML/HTML Code复制内容到剪贴板
  1. window.RAF = (function(){   
  2.        return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) {window.setTimeout(callback, 1000 / 60); };   
  3.             })();   

【避免浮点运算】

虽然javascript提供了很方便的一些取整方法,像Math.floor,Math.ceil,parseInt,但是,国外友人做过测试,parseInt这个方法做了一些额外的工作(比如检测数据是不是有效的数值,parseInt 甚至先将参数转换成了字符串!),所以,直接用parseInt的话相对来说比较消耗性能,那怎样取整呢,可以直接用老外写的很巧妙的方法了:

    JavaScript Code复制内容到剪贴板

    1.rounded = (0.5 + somenum) | 0;      

    2.rounded = ~~ (0.5 + somenum);   3.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

HTML / CSS 相关文章推荐
CSS3 transform的skew属性值图文详解
Jul 21 HTML / CSS
用React加CSS3实现微信拆红包动画效果
Mar 13 HTML / CSS
CSS3 linear-gradient线性渐变生成加号和减号的方法
Nov 21 HTML / CSS
你可能不熟练的十个前端HTML5经典面试题
Jul 03 HTML / CSS
Html5新标签解释及用法
Feb 17 HTML / CSS
突破canvas语法限制 让他支持链式语法
Dec 24 HTML / CSS
HTML5中的Article和Section元素认识及使用
Mar 22 HTML / CSS
使用canvas绘制贝塞尔曲线
Dec 17 HTML / CSS
HTML5新标签兼容——&gt; 的两种方法
Sep 12 HTML / CSS
详解canvas绘制网络字体几种方法
Aug 27 HTML / CSS
HTML+css盒子模型案例(圆,半圆等)“border-radius” 简单易上手
May 10 HTML / CSS
HTML怎么设置下划线?html文字加下划线方法
Dec 06 HTML / CSS
利用HTML5 Canvas制作一个简单的打飞机游戏
May 11 #HTML / CSS
用HTML5制作数字时钟的教程
May 11 #HTML / CSS
用HTML5 实现橡皮擦的涂抹效果的教程
May 11 #HTML / CSS
用HTML5中的Canvas结合公式绘制粒子运动的教程
May 08 #HTML / CSS
使用分层画布来优化HTML5渲染的教程
May 08 #HTML / CSS
简单介绍HTML5中的文件导入
May 08 #HTML / CSS
使用HTML5的Notification API制作web通知的教程
May 08 #HTML / CSS
You might like
PHP编程函数安全篇
2013/01/08 PHP
PHP自动识别当前使用移动终端
2018/05/21 PHP
JAVASCRIPT HashTable
2007/01/22 Javascript
如何使用Jquery获取Form表单中被选中的radio值
2013/08/09 Javascript
多种方式实现JS调用后台方法进行数据交互
2013/08/20 Javascript
复制网页内容,粘贴之后自动加上网址的实现方法(脚本之家特别整理)
2014/10/16 Javascript
9款2014最热门jQuery实用特效推荐
2014/12/07 Javascript
js检测判断日期大于多少天的方法
2015/05/04 Javascript
详谈JS中实现种子随机数及作用
2016/07/19 Javascript
jQuery ajax实现省市县三级联动
2021/03/07 Javascript
详解Vue-cli 创建的项目如何跨域请求
2017/05/18 Javascript
详解在vue-cli项目中使用mockjs(请求数据删除数据)
2017/10/23 Javascript
JS实现分页浏览横向图片(类轮播)实例代码
2017/11/06 Javascript
python获取本地计算机名字的方法
2015/04/29 Python
python 性能优化方法小结
2017/03/31 Python
Django objects.all()、objects.get()与objects.filter()之间的区别介绍
2017/06/12 Python
pandas 两列时间相减换算为秒的方法
2018/04/20 Python
python/sympy求解矩阵方程的方法
2018/11/08 Python
python实现祝福弹窗效果
2019/04/07 Python
解决PyCharm IDE环境下,执行unittest不生成测试报告的问题
2020/09/03 Python
详解python定时简单爬取网页新闻存入数据库并发送邮件
2020/11/27 Python
Lacoste美国官网:经典POLO衫品牌
2016/10/12 全球购物
西班牙多品牌鞋店连锁店:Krack
2018/11/30 全球购物
新闻编辑自荐信
2013/11/03 职场文书
见习期自我鉴定
2013/11/07 职场文书
汽车专业人才自我鉴定范文
2013/12/29 职场文书
《跨越海峡的生命桥》教学反思
2014/02/24 职场文书
党性心得体会
2014/09/03 职场文书
化工实习心得体会
2014/09/09 职场文书
2014年店长工作总结
2014/11/17 职场文书
2014年幼儿园班级工作总结
2014/12/17 职场文书
2014年度个人总结范文
2015/03/09 职场文书
《分数的意义》教学反思
2016/02/20 职场文书
Python开发之QT解决无边框界面拖动卡屏问题(附带源码)
2021/05/27 Python
Vue提供的三种调试方式你知道吗
2022/01/18 Vue.js
详解Python+OpenCV进行基础的图像操作
2022/02/15 Python