声明:本文为原创文章,如需转载,请注明来源WAxes,谢谢!

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

  【使用缓存】

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

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

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

  1 、 没使用缓存

  2 、 使用了缓存

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

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>

  

  离屏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

  本人前端小菜,写的不好请见谅。

谈谈canvas的性能优化(主要讲缓存问题)的更多相关文章

  1. Hibernate性能优化之EHCache缓存

    像Hibernate这种ORM框架,相较于JDBC操作,需要有更复杂的机制来实现映射.对象状态管理等,因此在性能和效率上有一定的损耗. 在保证避免映射产生低效的SQL操作外,缓存是提升Hibernat ...

  2. [MySQL性能优化系列]提高缓存命中率

    1. 背景 通常情况下,能用一条sql语句完成的查询,我们尽量不用多次查询完成.因为,查询次数越多,通信开销越大.但是,分多次查询,有可能提高缓存命中率.到底使用一个复合查询还是多个独立查询,需要根据 ...

  3. SQL性能优化前期准备-清除缓存、开启IO统计

    文章来至:https://www.cnblogs.com/Ren_Lei/p/5669662.html 如果需要进行SQl Server下的SQL性能优化,需要准备以下内容: 一.SQL查询分析器设置 ...

  4. canvas的性能优化

    canvas玩多了后,就会自动的要开始考虑性能问题了.怎么优化canvas的动画呢? [使用缓存] 使用缓存也就是用离屏canvas进行预渲染了,原理很简单,就是先绘制到一个离屏canvas中,然后再 ...

  5. 秋色园QBlog技术原理解析:性能优化篇:缓存总有失效时,构造持续的缓存方案(十四)

    转载自:http://www.cyqdata.com/qblog/article-detail-38993 文章回顾: 1: 秋色园QBlog技术原理解析:开篇:整体认识(一) --介绍整体文件夹和文 ...

  6. OpenStack入门篇(五)之KVM性能优化及IO缓存介绍

    1.KVM的性能优化,介绍CPU,内存,IO性能优化 KVM CPU-->qemu进行模拟ring 3-->用户应用 (用户态,用户空间)ring 0-->操作系统 (内核态,内核空 ...

  7. 性能优化之Hibernate缓存讲解、应用和调优

    JavaMelody——一款性能监控.调优工具, 通过它让我觉得项目优化是看得见摸得着的,优化有了针对性.而无论是对于分布式,还是非分布,缓存是提示性能的有效工具. 数据层是EJB3.0实现的,而EJ ...

  8. [原]性能优化之Hibernate缓存讲解、应用和调优

    近来坤哥推荐我我们一款性能监控.调优工具--JavaMelody,通过它让我觉得项目优化是看得见摸得着的,优化有了针对性.而无论是对于分布式,还是非分布,缓存是提示性能的有效工具. 数据层是EJB3. ...

  9. js 性能优化整理之 缓存变量

    简单的常见的操作:假设每个便签添加一个  属性   -webkit-animation-delay:0.1s 递增操作::通过for循环添加 <ul id="uls"> ...

随机推荐

  1. spring coud Feign常用配置

    Ribbon配置 在Feign中配置Ribbon非常简单,直接在application.properties中配置即可,如: # 设置连接超时时间 ribbon.ConnectTimeout=500 ...

  2. OpenCV trackbar 避免使用全局变量

    OpenCV trackbar 避免使用全局变量 在OpenCV中使用trackbar是交互式地图像处理的一种方式,例如用于图像阈值分割,用户可以一边调整阈值一边看效果.但是很不幸,绝大多数教程上使用 ...

  3. JAVA二分搜索树

    二叉树: 和链表一样,动态数据结构. 二叉树具有唯一根节点 二叉树具有天然的递归结构 二分搜索树是二叉树 二分搜索树的每个节点的值: 1.大于其左子树的所有节点的值 2.小于其右子树的所有节点的值 每 ...

  4. Linux网络编程目录

    基本TCP套接字编程 1. 套接字API 简单的回射服务器 TCP:IO多路复用 1. 函数select.poll 2.函数epoll

  5. Android自定义View+贝赛尔曲线

    Android -- 贝塞尔曲线公式的推导和简单使用https://www.cnblogs.com/wjtaigwh/p/6647114.html Android -- 贝塞尔使圆渐变为桃心http: ...

  6. Hadoop java.lang.ClassNotFoundException: org.apache.commons.lang3.StringUtils

    .jar 学习好友推荐案例的时候,提交运行时报错找不到StringUtils java.lang.ClassNotFoundException: org.apache.commons.lang3.St ...

  7. oracle安装过程中先决条件检查失败的解决办法

    1:公司数据库开始用的mysql,因为公司做的是保密性项目,所以就在项目日志过多的时候项目有爆炸的迹象啊(3000千万数据,貌似mysql有点撑不住).然后组长开始让我安装oracle,公司的内网也是 ...

  8. OSGi HelloWorld

    1.创建项目 2.Debug Configurations,配好之后,可以点一下右下角的Validate Bundles验证一下是否有问题 3.Debug

  9. 关于sql和MySQL的语句执行顺序

    sql和mysql执行顺序,发现内部机制是一样的.最大区别是在别名的引用上. 一.sql执行顺序 (1) from (3) join (2) on (4) where (5) group by(开始使 ...

  10. php把一些预定义的 HTML 实体转换为字符。

    htmlspecialchars_decode() echo htmlspecialchars_decode($condition,ENT_QUOTES) '  会被转成 单引号