高性能动画设计的一些优化思路总结

问题1:关注JS的垃圾回收对动画性能的影响

Javascript没有显式的内存管理,这就意味着你创建了对象但是你没有释放他们,久而久之,浏览器就会去清理这些对象。这时候动画执行就会停止,浏览器会识别那些内存依然在使用,然后释放其它的内存空间。而且这种多余的垃圾都是逐渐被创建,然后一起被清理的。在60fps的动画上,每一帧只有16ms的绘制时间,但是垃圾回收却会100ms或者更长,这样就会导致明显的动画停顿。因此,在动画设计中我们在每一帧中应该尽量减少对象的创建。

简单的准则:

我们使用的new操作符往往意味着资源的分配,如new Foo()等,因此我们应该在一开始的时候就创建对象,以后尽量使用同一个对象,当然以下三种方式也是new操作的简洁方式:

{}
[]
function(){}
我们应该尽量减少{}的使用,就像减少new一样。因此在函数返回值中我们可以使用一个全局的变量去替代每次都创建一个返回值对象,如{'foo':'bar'}等。当然你也可以回收一个已经存在的对象,如释放他的属性,或者赋值为一个空对象。

cr.wipe=function(obj){
	for(var p in obj){
		if(obj.hasOwnProperty(p)){
			delete obj[p];
		}
	}
}
使用wipe方法有时候比简单的赋值为{}要慢,但是后者往往会产生垃圾并需要后续的回收操作并可能对后续动画产生影响。

清除[数组]的时候,我们常常使用赋值为[],但是这回创建一个新的数组对象,同时会对上一个数组对象进行垃圾回收,所以我们建议使用arr.length=0的操作,这样可以对原来的数组进行回收
对于函数来说,他是在一开始的时候就会创建,因此不会在运行的时候分配空间,但是这会导致我们忽视一个细节:当函数动态创建的时候,如函数返回一个函数的情况。我们常常使用setTimeout或者requestAnimationFrame 来创建动画:

setTimeout(function(self){
	 return funtion(){
         self.tick();
	};})(this),16)
这样虽然会每隔16ms调用一次tick函数,但是每次都会返回一个全新的函数,因此我们可以使用一个变量来保存函数:
this.tickFun=(function(self){
	  return function(){
	  	 self.tick();
	  }
	})(this);
setTimeout(this.tickFun,16)
这个函数就会得到重用,而不是每次在运行的时候都会创建一个对象
基础准则:

很多JS库会产生大量的对象,如数组的slice方法,会返回一个新的数组(基于原来的数组,而且原来的数组没有变化),或者字符串的substr方法,他会产生一个新的字符串,而且原来的字符串也不会变化,因此对于这些方法的调用都会产生垃圾,你可以做的要么是不调用要么就是重写这个函数使得他不会产生新的垃圾,如把下面的方法:

var sliced=arr.slice(index+1);
arr.length=index;
arr.push.apply(arr,sliced);
修改为如下方法:
for(var i=index,len=arr.length-1;i<len;i++){
	arr[i]=arr[i+1]
}
arr.length=len;
在递归函数中使用{}对象来传递数据是我们经常做的事情,但是往往采用一个数组效率更高,这个数组可以作为一个栈来使用,通过一个变量来保存每一层入栈和出栈的层级。

在递归函数中我们通常使用{}来在不同层级之间传递数据。我们可以通过一个数组来模拟入栈和出栈操作。不要经常从数组中弹出元素,因为这样你就要每次都回收数组的最后一个元素。我们可以使用一个topIndex参数,如果是入栈操作我们可以增加topIndex,反之可以减少topIndex。当下次继续入栈的时候我们就可以回收利用刚才的那个元素。这就是对象池的概念:

// declare bullet pools
var activeBullets = [];
var bulletPool = [];

// construct some bullets ready for use
for (var i=0; i < 20; i++)
    bulletPool.push( new Bullet() );

// a constructor/factory function
function getNewBullet()
{
    var b = null;

    // check to see if there is a spare one
    if (bulletPool.length > 0)
        b = bulletPool.pop();
    else 
        // none left, construct a new one
        b = new Bullet();	

    // move the new bullet to the active array
    activeBullets.push(b);
    return b;
}

function freeBullet(b)
{
    // find the active bullet and remove it
    // NOTE: Not using indexOf since it wont work in IE8 and below
    for (var i=0, l=activeBullets.length; i < l; i++)
        if (activeBullets[i] == b)
            array.slice(i, 1);
	
    // return the bullet back into the pool
    bulletPool.push(b);
}

问题2:浏览器垃圾回收和帧率
我们知道每一帧的绘制只有短短的16ms,因此我们必须考虑到:JS的虚拟机需要经常停顿下来去收集垃圾和管理内存。我们可以使用如下方法来监控堆内存的变化:

function() {
  var lastHeapSize = null;
  var lastFrameTime = null;
  var runGame = function() {
    requestAnimationFrame(runGame);//下一帧的绘制
    var frameTime = window.performance.now();
    var heapSize = window.performance.memory.usedJSHeapSize;
    if (lastHeapSize == null) { lastHeapSize = heapSize; }
    if (lastFrameTime == null) { lastFrameTime = frameTime; }
    var dt = frameTime - lastFrameTime;
    var dh = heapSize - lastHeapSize
    frameDataList.push([dt, dh]);
    lastHeapSize = heapSize;
    lastFrameTime = frameTime;
    computeNextGameStateAndPaint();
  };
}();
我们看看如下的耗时操作:

var makeSimulatedGameLoop = function(n) {
  /* Pre-initialize an array with distinct simple objects */
  var g = new Array(1000*n);
  var count = 0;
  for (i=0; i< g.length; i++) {
    g[i] = {count: count++};
  }
  var _work = function() {
    var i;
    for (i=0; i < g.length; i++) {
      /* For every game variable, double it's count property. */
      /* Implicitly create a new object, garbaging the old one. */
      g[i] = { count: g[i].count * 2 };
    };
  };
  return _work;
};
computeNextGameStateAndPaint = makeSimulatedGameLoop(20);  

当上面的代码执行一段时间后我们就会获取到如下的内容frameDataList。

[16.672734, 133128],
[16.183574, 128228],
[39.847293, -12158528],
[16.974714, 119222],
[16.672734, 140248]
JS的虚拟机不会明确指定垃圾的回收,但是如果两次之间的JS的堆内存存在减少那么肯定是存在垃圾回收的,这样我们只可能少估计了垃圾回收的次数,而不会高估。

通过绘制两帧之间时间差别和JS堆内存的差别可以知道:过多的垃圾回收会严重的影响动画的帧率,但是严重的垃圾回收往往发生在应用的前期。因此我们需要站在每一帧的CPU限制角度以及垃圾回收的频率角度来获取稳定的高帧率

问题3:减少布局抖动

fastdom.read(function() {
  var h1 = element1.clientHeight;
  fastdom.write(function() {
    element1.style.height = (h1 * 2) + 'px';
  });
});

fastdom.read(function() {
  var h2 = element2.clientHeight;
  fastdom.write(function() {
    element2.style.height = (h1 * 2) + 'px';
  });
});

减少布局抖动对于提升页面的性能具有重要的影响。fastdom这个库首先通过遍历读队列,然后遍历写队列完成了这个读写操作的批量化处理。而且特别要注意:fastdom你往里面添加的读写方法全部要等到下一帧才能执行,因此在下一帧执行完毕后fastdom.scheduled才会为false。因此不会在每次添加的时候都执行队列的遍历操作,这一点一定要注意。当你可以在下一帧回调中继续添加处理函数,这样就可以在下下帧进行批量修改。通过下面的例子你就会明白,我们可以如何减少布局抖动的:

 var divs = document.querySelectorAll('div');
var raf = window.requestAnimationFrame;
var each = [].forEach;
var now = function() {
  return performance
    ? performance.now()
    : Date.now();
};
/**
 * Thrashing solution
 */
thrash.onclick = function() {
  reset();
  var start = now();
  // Loop each div
  each.call(divs, function(div) {
    var width = div.clientWidth;
    div.style.height = width + 'px';
  }); 
  // Render result
  renderSpeed(now() - start);
};
/**
 * Non-thrashing solution
 */

nothrash.onclick = function() {
  reset(); 
  var start = now();
  // Loop each div
  each.call(divs, function(div) {
    var width = div.clientWidth;    
    // Schedule the write
    // operation to be run
    // in the next frame.
    raf(function() {
      div.style.height = width + 'px';
    });
  });
  // Render result
  raf(function() {
    renderSpeed(now() - start);
  });
};
// Resets the divs
function reset() {
  each.call(divs, function(div) {
    div.style.height = '';
    div.offsetTop;
  });
}
function renderSpeed(ms) {
  speed.textContent = ms + 'ms';
}

我们可以使用fastdom为我们提供的思路来实现高性能的动画,如下面的例子就是很好的验证:

			var moveMethod = 'sync',
			//默认的动画执行使用同步的动画
				count      = document.getElementById('count'),
				//元素的数量
				test       = document.getElementById('test'),
				//最后这个test元素
				timestamp, raf, movers;
				//timestamp:该值为浏览器传入的从1970年1月1日到当前所经过的毫秒数

			var mover = {
				sync: function(m) {
					// Read the top offset, and use that for the left position
					mover.setLeft(movers[m], movers[m].offsetTop);
				},
				async: function(m) {

					// Use fastdom to batch the reads
					// and writes with exactly the same
					// code as the 'sync' routine
					fastdom.measure(function() {
						var top = movers[m].offsetTop;
						//上一帧的数据作为下一帧的参数值
						fastdom.mutate(function() {
							mover.setLeft(movers[m], top);
						});
					});
				},
				noread: function(m) {
					// Simply use the array index
					// as the top value, so no DOM
					// read is required
					mover.setLeft(movers[m], m);
				},
				setLeft: function(mover, top) {
					//如果是把上面的offsetTop修改为offsetLeft,这时候我们可以看到左右移动的距离仅仅和当前元素左右距离有关,于是就是水平移动的效果了
					mover.style.transform = 'translateX( ' +((Math.sin(top + timestamp/1000) + 1) * 500) + 'px)';
					//这里可以修改移动的距离
				}
			};

			function update(thisTimestamp) {
				timestamp = thisTimestamp;
				//会为每一个要执行的函数传入一个时间点
				for (var m = 0; m < movers.length; m++) {
					mover[moveMethod](m);
				}
				raf = window.requestAnimationFrame(update);
				//返回一个handle值,该值为浏览器定义的大于0的整数,唯一标志了该回调函数在列表中的位置
			}

			function toggleAnim(e) {
				var html, num;
				//如果raf存在,那么表示动画存在,这时候我们简单取消动画就可以了
				if (raf) {
					window.cancelAnimationFrame(raf);
					//如果raf存在,那么取消
					raf = false;
					e.currentTarget.innerHTML = 'Start';
					//此时不能修改count元素的属性
					count.disabled = false;

				} else {
					html = '';
					num = count.value;
					for (i = 0; i < num; i++) {
						html += '<div class="mover"></div>';
					}
					test.innerHTML = html;
					movers = test.querySelectorAll('.mover');
					movers[0].style.top = '150px';
					//第一个元素是距离上面150px
					for (var m = 1; m < movers.length; m++) {
						//每一个距离上面加20px
						movers[m].style.top = (m * 20) + 150 + 'px';
					}

					raf = window.requestAnimationFrame(update);
					e.currentTarget.innerHTML = 'Stop';
					//变成stop就可以了,同时disabled设置为true表示不允许修改了
					count.disabled = true;
				}
			}

			function setMethod(method) {
				document.getElementById(moveMethod).classList.remove('active');
				document.getElementById(method).classList.add('active');
				moveMethod = method;
			}
                      //toggle来完成
			document.getElementById('toggle').addEventListener('click', toggleAnim);
			//同步动画来完成
			document.getElementById('sync').addEventListener('click', function() {
				setMethod('sync');
			});
			//异步动画来完成
			document.getElementById('async').addEventListener('click', function() {
				setMethod('async');
			});
			//没有读取操作
			document.getElementById('noread').addEventListener('click', function() {
				setMethod('noread');
			});

下面这个例子也给我们展示了使用不同的方式来批量修改DOM属性而产生的不同的性能开销问题。

 var n;
    var start;
    var divs;
    // Setup
    //参数为:
    /*
      function() {
        start = performance.now();
        divs.forEach(setAspect);
      }
    */
    function reset(done) {
      n = input.value;
      divs = [];
      fastdom.measure(function() {
        var winWidth = window.innerWidth;
        fastdom.mutate(function() {
          container.innerHTML = '';
          for (var i = 0; i < n; i++) {
            var div = document.createElement('div');
            //元素的宽度和窗口的宽度有关系了
            div.style.width = Math.round(Math.random() * winWidth) + 'px';
            container.appendChild(div);
            divs.push(div);
          }
           //执行我们传入的函数就可以了
          if (done) done();
        });
      });
    }
//调用方式: divs.forEach(setAspect);setAspect中第一个参数为元素本身第二个为下标
    function setAspect(div, i) {
      var aspect = 9 / 16;
      var isLast = i === (n - 1);
      var h = div.clientWidth * aspect;
      div.style.height = h + 'px';
     //每一个元素都是符合经典的16:9的
      if (isLast) {
        displayPerf(performance.now() - start);
      }
    }

    function setAspectRequestAnimationFrame(div, i) {
      var aspect = 9 / 16;
      var isLast = i === (n - 1);
      // READ
      requestAnimationFrame(function() {
        var h = div.clientWidth * aspect;
        // WRITE
        requestAnimationFrame(function() {
          div.style.height = h + 'px';
          if (isLast) {
            //显示总共花费的时间
            displayPerf(performance.now() - start);
          }
        });
      });
    }

    function setAspectFastDom(div, i) {
      var aspect = 9 / 16;
      var isLast = i === (n - 1);
      // READ
      fastdom.measure(function() {
        var h = div.clientWidth * aspect;
        // WRITE
        fastdom.mutate(function() {
          div.style.height = h + 'px';
          if (isLast) {
            displayPerf(performance.now() - start);
          }
        });
      });
    }

    function displayPerf(ms) {
      perf.textContent = ms + 'ms';
    }
   //不使用fastdom来完成的
    withoutFastDom.onclick = function() {
      reset(function() {
        start = performance.now();
        divs.forEach(setAspect);
      });
    };

    withFastDom.onclick = function() {
      reset(function() {
        start = performance.now();
        divs.forEach(setAspectFastDom);
      });
    };
//使用RequestAnimationFrame方式,把所有的读操作和写操作分开,写操作放在下一帧中统一完成
    withRequestAnimationFrame.onclick = function() {
      reset(function() {
        start = performance.now();
        divs.forEach(setAspectRequestAnimationFrame);
      });
    };
    resetbtn.onclick = function() {
      reset();
    };
实例代码可以 点此下载

问题4:减少内存泄漏的可能性

var xhr = new XMLHttpRequest()
xhr.open('GET', 'jquery.js', true)
xhr.onreadystatechange = function() {
  if(this.readyState == 4 && this.status == 200) {           
    document.getElementById('test').innerHTML++
  }
}
xhr.send(null)
xhr=null;

在ie9以前的浏览器回调结束后不会自动销毁xhr对象,因此后面需要手动设置为null。但是函数内部的引用全部改为this,因为this不会从外部作用域中继承。通过把外部变量设置为null是我们解决内存泄漏一个常用的方法,即使仅仅是外部作用域一个大的字符串也可以采用这种方式减少循环引用!

参考资源:
How to write low garbage real-time Javascript

Browser Garbage Collection and Frame Rate

Accuracy of JavaScript Time

Preventing 'layout thrashing'

High-Performance, Garbage-Collector-Friendly Code

Memory leaks

JavaScript Memory Profiling (Chrome开发者工具之JavaScript内存分析

【内存泄漏】怎么用Chrome DevTool发现内存泄漏

Javascript高性能动画与页面渲染 (同步优先级高于异步的setTimeout,而且setTimeout的精确度受浏览器内置的时钟和时钟频率影响)

动画时间函数的确定

前端动画原理与实现

骨骼动画原理与前端实现浅谈

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值