Web动画优化之requestAnimationFrame深入理解

前言

在 requestAnimationFrame 之前,setTimeout和setInterval定时器一直是web动画的核心技术,而动画的关键在于动画帧之间的时间间隔设置,这个时间间隔的设置有讲究,一方面要足够小,这样动画帧之间才有连贯性,动画效果才显得平滑流畅;另一方面要足够大,确保浏览器有足够的时间及时完成渲染。而setTimeout和setInterval的问题是,它们都不精确。它们的内在运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器UI线程队列中以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务完成后再执行。

既然setTimeout和setInterval在实现web动画这块并不完美,那还有更好的解决方案吗?当然,requestAnimationFrame是一个不错的选择。在讲requestAnimationFrame之前,先来说说屏幕刷新频率。

屏幕刷新率

目前大多数设备的屏幕刷新频率为60Hz,大概相当于每秒钟刷新60次。如果在页面中有一个动画效果,那么浏览器渲染动画的每一帧的速率也需要跟设备屏幕的刷新率保持一致才能保证动画的流畅体验。

以60Hz刷新频率的屏幕为例,那么动画每个帧的预算时间仅比16毫秒多一点(1秒/ 60 ≈ 16.67毫秒),如果在屏幕1次刷新周期内多次执行动画切帧,就会造成过度绘制,增加开销;如果动画切换在 16ms, 32ms, 48ms时分别切换,假如到了32ms,其他任务还未执行完成,没有去执行动画切帧,等到开始进行动画的切帧,已经到了该执行48ms的切帧,就会造成跳帧,就好比你玩游戏的时候卡了,过了一会,你再看画面,它不会停留你卡的地方,或者这时你的角色已经挂掉了。

requestAnimationFrame实现动画

requestAnimationFrame是浏览器用于定时循环操作的一个接口。与setTimeout和setInterval不同,requestAnimationFrame采用系统时间间隔,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间太长,使用动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。

requestAnimationFrame的用法与setTimeoutt很相似,只是不需要设置时间间隔而已。requestAnimationFrame使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。它返回一个整数,表示定时器的编号,这个值可以传递给cancelAnimationFrame用于取消这个函数的执行。

兼容

目前大部分现代浏览器均已实现requestAnimationFrame,少部分低版本浏览器(如IE9-)可通过优雅降级的方式对requestAnimationFrame进行封装,优先使用高级特性,然后再根据不同浏览器的情况进行回退,直到只能使用setTimeout的情况。

下面这段是一个简单的requestAnimationFrame兼容写法:

// shim layer with setTimeout fallback
window.requestAnimFrame = (function(){
  return  window.requestAnimationFrame       ||
          window.webkitRequestAnimationFrame ||
          window.mozRequestAnimationFrame    ||
          function( callback ){
            window.setTimeout(callback, 1000 / 60);
          };
})();


// usage:
// instead of setInterval(render, 16) ....

(function animloop(){
  requestAnimFrame(animloop);
  render();
})();
// place the rAF *before* the render() to assure as close to
// 60fps with the setTimeout fallback.

以下代码是Opera工程师Erik Möller所写更加健壮的polyfill。你可以阅读下,尤其是在处理4-16ms的延迟时间这段代码,从而使帧率更接近60fps:

(function() {
  var lastTime = 0;
  var vendors = ['webkit', 'moz'];
  for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
      window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
      window.cancelAnimationFrame =
        window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];
  }

  if (!window.requestAnimationFrame)
      window.requestAnimationFrame = function(callback, element) {
          var currTime = new Date().getTime();
          var timeToCall = Math.max(0, 16 - (currTime - lastTime));
          var id = window.setTimeout(function() { callback(currTime + timeToCall); },
            timeToCall);
          lastTime = currTime + timeToCall;
          return id;
      };

  if (!window.cancelAnimationFrame)
      window.cancelAnimationFrame = function(id) {
          clearTimeout(id);
      };
}());

总结

定时器一直是web动画的核心技术,使用requestAnimationFrame相较于setTimeout和setInterval具体优势有以下几点:

  1. requestAnimationFrame会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率。
  2. 在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的CPU、GPU和内存使用量。
  3. requestAnimationFrame是由浏览器专门为动画提供的API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销。

Related Posts