谈谈canvas的性能优化(主要讲缓存问题)
声明:本文为原创文章,如需转载,请注明来源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的性能优化(主要讲缓存问题)的更多相关文章
- Hibernate性能优化之EHCache缓存
像Hibernate这种ORM框架,相较于JDBC操作,需要有更复杂的机制来实现映射.对象状态管理等,因此在性能和效率上有一定的损耗. 在保证避免映射产生低效的SQL操作外,缓存是提升Hibernat ...
- [MySQL性能优化系列]提高缓存命中率
1. 背景 通常情况下,能用一条sql语句完成的查询,我们尽量不用多次查询完成.因为,查询次数越多,通信开销越大.但是,分多次查询,有可能提高缓存命中率.到底使用一个复合查询还是多个独立查询,需要根据 ...
- SQL性能优化前期准备-清除缓存、开启IO统计
文章来至:https://www.cnblogs.com/Ren_Lei/p/5669662.html 如果需要进行SQl Server下的SQL性能优化,需要准备以下内容: 一.SQL查询分析器设置 ...
- canvas的性能优化
canvas玩多了后,就会自动的要开始考虑性能问题了.怎么优化canvas的动画呢? [使用缓存] 使用缓存也就是用离屏canvas进行预渲染了,原理很简单,就是先绘制到一个离屏canvas中,然后再 ...
- 秋色园QBlog技术原理解析:性能优化篇:缓存总有失效时,构造持续的缓存方案(十四)
转载自:http://www.cyqdata.com/qblog/article-detail-38993 文章回顾: 1: 秋色园QBlog技术原理解析:开篇:整体认识(一) --介绍整体文件夹和文 ...
- OpenStack入门篇(五)之KVM性能优化及IO缓存介绍
1.KVM的性能优化,介绍CPU,内存,IO性能优化 KVM CPU-->qemu进行模拟ring 3-->用户应用 (用户态,用户空间)ring 0-->操作系统 (内核态,内核空 ...
- 性能优化之Hibernate缓存讲解、应用和调优
JavaMelody——一款性能监控.调优工具, 通过它让我觉得项目优化是看得见摸得着的,优化有了针对性.而无论是对于分布式,还是非分布,缓存是提示性能的有效工具. 数据层是EJB3.0实现的,而EJ ...
- [原]性能优化之Hibernate缓存讲解、应用和调优
近来坤哥推荐我我们一款性能监控.调优工具--JavaMelody,通过它让我觉得项目优化是看得见摸得着的,优化有了针对性.而无论是对于分布式,还是非分布,缓存是提示性能的有效工具. 数据层是EJB3. ...
- js 性能优化整理之 缓存变量
简单的常见的操作:假设每个便签添加一个 属性 -webkit-animation-delay:0.1s 递增操作::通过for循环添加 <ul id="uls"> ...
随机推荐
- jsp和
- map reduce程序示例
map reduce程序示例 package test2; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop. ...
- 使用docker部署.net core应用
CentOS 使用VS2017新建一个asp.net core项目 发布web应用程序 使用FTP工具,将程序文件copy到linux上 XShell连上linux并登陆 cd /CoreWebDem ...
- 背包的一些idea
题解: 给出n个物品,每次能使用l-r之间的物品,问能不能表示出k,m次询问 k<=100,m,n=1e5 想了线段树分治 发现是k^2(n+m)logn claris告诉我可以直接分治 我们对 ...
- xcode svn commit is not under version control 和 git常用指令
使用Xcode提交一个第三方库时,由于包含资源文件,总是提交不了,提示报错:XXX commit is not under version control (1) 网上查了下,得知 xcode对于sv ...
- python基础——高级特性
1.切片 切片: >>> L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack'] >>> L[:3] ['Michael ...
- autio的自动播放问题
最近做年会相关内容,背景音乐插入了,电脑上没问题,移动版就出事了,下面做一下记录 <audio src="" autoplay="autoplay" l ...
- Python学习(三) —— 基本数据类型
基本数据类型 一.整数类型(int) 32位机器,整数的位数为32位,取值范围为-2**31~2**31-1,即-2147483648~2147483647 64位机器,整数的位数是64位,取值范围位 ...
- web列表总结
ztree:web树 下拉列表树: jqxgrid列表(带有多选,单选.搜索.分页.分页多选.还有点击下拉等功能) 还有flexigrid(百度搜索)
- LoadRunner脚本参数化之自动关联和手动关联
一.关联的意义 1.关联的含义 关联(correlation):在脚本回放过程中,客户端发出请求,通过关联函数所定义的左右边界值(也就是关联规则),在服务器所响应的内容中查找,得到相应的值,以变量的 ...