requestAnimationFrame详解以及无线页面优化

requestAnimationFrame是浏览器用于定时循环操作的一个接口,类似于setTimeout,主要用途是按帧对网页进行重绘。

设置这个API的目的是为了让各种网页动画效果(DOM动画、Canvas动画、SVG动画、WebGL动画)能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。代码中使用这个API,就是告诉浏览器希望执行一个动画,让浏览器在下一个动画帧安排一次网页重绘。

requestAnimationFrame的优势,在于充分利用显示器的刷新机制,比较节省系统资源。显示器有固定的刷新频率(60Hz或75Hz),也就是说,每秒最多只能重绘60次或75次,requestAnimationFrame的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。此外,使用这个API,一旦页面不处于浏览器的当前标签,就会自动停止刷新。这就节省了CPU、GPU和电力。

不过有一点需要注意,requestAnimationFrame是在主线程上完成。这意味着,如果主线程非常繁忙,requestAnimationFrame的动画效果会大打折扣。

requestAnimationFrame使用一个回调函数作为参数。这个回调函数会在浏览器重绘之前调用。

requestID = window.requestAnimationFrame(callback); 

目前,主要浏览器Firefox 23 / IE 10 / Chrome / Safari)都支持这个方法。可以用下面的方法,检查浏览器是否支持这个API。如果不支持,则自行模拟部署该方法。

 window.requestAnimFrame = (function(){
      return  window.requestAnimationFrame       || 
              window.webkitRequestAnimationFrame || 
              window.mozRequestAnimationFrame    || 
              window.oRequestAnimationFrame      || 
              window.msRequestAnimationFrame     || 
              function( callback ){
                window.setTimeout(callback, 1000 / 60);
              };
    })();

上面的代码按照1秒钟60次(大约每16.7毫秒一次),来模拟requestAnimationFrame。

使用requestAnimationFrame的时候,只需反复调用它即可。

function repeatOften() {
  // Do whatever
  requestAnimationFrame(repeatOften);
}

requestAnimationFrame(repeatOften);

cancelAnimationFrame方法

cancelAnimationFrame方法用于取消重绘。

window.cancelAnimationFrame(requestID);

它的参数是requestAnimationFrame返回的一个代表任务ID的整数值。

var globalID;

function repeatOften() {
  $("<div />").appendTo("body");
  globalID = requestAnimationFrame(repeatOften);
}

$("#start").on("click", function() {
  globalID = requestAnimationFrame(repeatOften);
});

$("#stop").on("click", function() {
  cancelAnimationFrame(globalID);
});

上面代码持续在body元素下添加div元素,直到用户点击stop按钮为止。

实例

下面,举一个实例。

假定网页中有一个动画区块。

<div id="anim">点击运行动画</div> 

然后,定义动画效果。

var elem = document.getElementById("anim");

var startTime = undefined;
 
function render(time) {
 
  if (time === undefined)
    time = Date.now();
  if (startTime === undefined)
    startTime = time;
 
  elem.style.left = ((time - startTime)/10 % 500) + "px";
}

最后,定义click事件。

elem.onclick = function() {

    (function animloop(){
      render();
      requestAnimFrame(animloop);
    })();

};

运行效果可查看jsfiddle

无线页面动画优化实例

无线页面本就分秒必争,更不用说当我们在无线页面中使用动画的时候。不管是css动画还是canvas动画,我们都需要时刻小心着,并且有必要掌握页面性能的基本分析方法。

既然我们的目标是优化,那么就与浏览器的一些渲染和执行机制有关,更好的迎合浏览器的行为方式,才可以让我们的动画流畅而优美。

没错,浏览器是老大,全听它的。

546321-20160415155935270-1575731219

 

一、设备刷新率(帧率)

我们想让页面变快,想让动画流畅,我们需要先了解一下是什么在影响着我们的感知。

页面运行在设备的浏览器中,现在市面上的移动设备的刷新频率大多是60次/秒(帧率)。所以给浏览器渲染每一帧的画面的时间应该是(1s/60=16.67ms)。

但实际上,浏览器并不是把功夫全花在为我们渲染页面上,他还需要做一些额外的工作,比如渲染队列的管理和不同线程的切换等等。所以,单纯的浏览器渲染工作留给我们的时间大约也就是10ms左右,当我们在每一帧所做的渲染操作大于这个时间的时候,比较直观的表现就是页面卡顿,动画卡顿。

当我们使用css animation完成动画时,这一点看起来没有那么重要,因为浏览器会为我们handle一些事情。但是当我们需要使用js比如canvas来实现流畅的逐帧动画时,需要牢记这个有限的时间,它很重要。

二、浏览器的页面渲染流水线

我们的代码是如何一步步的渲染成页面的呢?

546321-20160415162455238-1088611650

  • JavaScript。一般来说,我们使用JavaScript来实现一些页面逻辑,但偶尔我们也可能会使用JavaScript来实现一些视觉变化的效果。比如用jQuery的animate函数做一个动画、或者往页面里添加一些DOM元素等。当然,现在更可能的是使用CSS Animations, Transitions和Web Animation API。
  • 计算样式(Style)。这个过程是通过样式文件中的CSS选择器,对每个DOM元素匹配对应的CSS样式。
  • 布局(Layout)。上一步确定了每个DOM元素的样式规则,这一步就是具体计算每个DOM元素最终在屏幕上显示的大小和位置。web页面中元素的布局是相对的,因此一个元素的布局发生变化,会联动地引发其他元素的布局发生变化。因此对于浏览器来说,布局过程是经常发生的。
  • 绘制(Paint)。绘制,本质上就是填充像素的过程。包括绘制文字、颜色、图像、边框和阴影等,也就是一个DOM元素所有的可视效果。一般来说,这个绘制过程是在多个层上完成的。
  • 渲染层合并(Composite)。由上一步可知,对页面中DOM元素的绘制是在多个层上进行的。在每个层上完成绘制过程之后,浏览器会将所有层按照合理的顺序合并成一个图层,然后显示在屏幕上。对于有位置重叠的元素的页面,这个过程尤其重要,因为一旦图层的合并顺序出错,将会导致元素显示异常。

看起来每个页面都会经历这样的几个过程,然而我们其实可以使用一些技巧,帮助浏览器跳过某些步骤,而缩短他的工作时间。

1.五个步骤都消耗了时间

当我们在js中改变了某个DOM元素的layout时,那么浏览器就会检查页面中的哪些元素需要重新布局,然后对页面激发一个reflow过程以完成页面的重新布局。被reflow的元素,接下来就一定会再次经过Paint和Composite这两个过程,以渲染出最新的页面。

2.跳过layout这一步

当我们只修改了一个DOM元素的paint only属性的时候,比如background-image/color/box-shadow等。这个时候不会触发layout,浏览器在完成样式的计算之后就会跳过layout的过程,就只Paint和Composite了。

3.跳过layout和paint这两步

如果你修改一个非样式且非绘制的CSS属性,那么浏览器会在完成样式计算之后,跳过布局和绘制的过程,直接Composite。

我们尝试下使用transform动画来尽可能的达到这种效果。

三、使用transform实现动画

我们可能经常需要做一些动画,比如在做某些揭秘或者新手引导的效果时,会需要做一些将内容移入移出的操作。

当然可能第一个想到的就是 css transition 只要过渡一下 left 值或者 bottom 的值就可以了。效果或许很快就会实现,但是当我们在一个页面频繁的做着这样的移入移出操作时,细心地我们放在手机中(6P)看一看,动画并不会很流畅,尤其是在某些低端机型上。

我们换用 transform 来实现相同的效果:

 transition: left 2s ease-in-out;  ---> transition: transform 2s ease-in-out;  
 left: xxx; ---> transform: translate3d(xxx, yyy, zzz);

原因在于:

  • 简单的说页面的绘制并不是在单层的画面里完成的,这其中有渲染层合成层等概念。对 opacity 和 transform 应用了 CSS 动画的渲染层、有 3D 或者 perspective transform 的 CSS 属性的渲染层等满足一些条件的渲染层被称为合成层;
  • 合成层有自己的渲染上下文,并且交由 GPU 处理,比 CPU 要快;
  • 当页面需要重绘时,合成层的元素只会重绘自己层内的元素,而非整个页面;

优化过后再放在设备里查看,可以感受到效果明显的提升。其实这里就做到了上面提到的,节省了layout和paint。

四、从css到canvas,使用requestAnimationFrame

现在css的动画越来越好用,也能满足越来越多的需求。但在某些复杂的需求中我们可能还是要求助于js。

比如说我这里实现的一个半圆的动画:[半圆progress] [Source Code]。看起来使用css动画就完全可以满足我的需求,但是当需求变化的时候,我们也只能拥抱变化了。

使用requestAnimationFrame

[圆弧progress][Source Code] 这里用canvas实现了自定义弧度圆弧的增长动画。

这里我们借助这个动画效果看一下是如何使用canvas和requestAnimationFrame来实现流畅的逐帧动画的。

window.requestAnimationFrame 是一个专门为动画而生的 web API 。它通知浏览器在页面重绘前执行你的回调函数。通常来说被调用的频率是每秒60次。

假设我们的页面上有一个动画效果,如果我们想保证每一帧的顺利绘制,那么我们就需要requestAnimationFrame来保证我们的绘制时机了。

很多框架和示例代码都是用setTimeoutsetInterval来实现页面中的动画效果,比如jQuery中的animation。这种实现方式的问题是,你在setTimeoutsetInterval中指定的回调函数的执行时机是无法保证的。它将在这一帧动画的_某个时间点_被执行,很可能是在帧结束的时候。这就意味这我们可能失去这一帧的信息。

546321-20160415173822207-178403401

 

requestAnimationFrame的其他高能用法

根据requestAnimationFrame的特性,其实我们还可以在很多别的想不到的地方来一显身手。

  • 动画:也是它的主要用途,它将我们动画的执行时机和执行频率交由浏览器决定,以得到更好的性能;
  • 函数节流:requestAnimationFrame 的执行频率(一帧)是16.67ms,利用这一个特征就可以做到函数节流,避免高频事件在一帧内做多余的无用功的函数执行,例:
 var $box = $('#J_Test'),
       $point = $box.find('b');
 $box.on('mouseenter',function(e){
   requestAnimationFrame(function(){
       $point.css({
           top : e.pageY,
           left : e.pageX
       })
   });
 });

分帧初始化:同样利用一帧的执行时间将模块的初始化或渲染函数分散到不同的帧中来执行,这样每个模块都有16.67ms的执行时间,而不是一股脑的堆在那里等着执行;

  var rAF = window.requestAnimationFrame ||  window.webkitRequestAnimationFrame || 
         function(c) {
             setTimeout(c, 1 / 60 * 1000);
         };
 
     function render() {
        self.$container.html(itemHtml);
        self.$container.find('.J_LazyLoad').lazyload();
     }
 
     rAF(render);

五、分析你的无线页面

我们还是借助这个例子,[圆弧progress][Source Code] 简单的看下如何分析无线页面的性能。

这里的实现思路是这样的:

1 - 确定圆弧的起始弧度(0.75PI)和终止弧度(根据当前分值占上限分值的比例计算,最大为2.25PI); 
2 - 随着时间的增长逐帧绘制终点位置 requestAnimationFrame(draw);
3 - 根据每一帧的终点位置的 cos 和 sin 值得到跟随的圆圈坐标并绘制;

但当然,实现完成只是走了第一步,我们来借助Chrome Timeline来分析一下这个简单的页面。

546321-20160415180105754-629717492

 

  1. 看一下帧率,在进度动画进行的时候,看起来帧率不错,没有产生掉帧的现象,说明每一帧的耗时都还ok,我的动画基本不会卡顿;
  2. 在函数的执行和调用那一栏中,可能有问题的部分右上角会被标红,还可以查看可能存在问题的细节;这里提示我页面强制重排了,仔细观察下面的 Bottom-up tab 中可以定位到具体的代码。

使用Timeline就可以看到页面的几种指标,帧率,js执行等等。就可以针对出现问题的帧下手优化。

在分析页面性能的时候,严重推荐阅读:[https://developer.chrome.com/devtools/docs/timeline] .timeline的详细使用说明,它真的很强大,能帮助我们分析到页面的各个方面的问题。

参考:

http://javascript.ruanyifeng.com/htmlapi/requestanimationframe.html

http://www.cnblogs.com/skylar/p/5374916.html


关注我

我的微信公众号:前端开发博客,在后台回复以下关键字可以获取资源。

  • 回复「小抄」,领取Vue、JavaScript 和 WebComponent 小抄 PDF
  • 回复「Vue脑图」获取 Vue 相关脑图
  • 回复「思维图」获取 JavaScript 相关思维图
  • 回复「简历」获取简历制作建议
  • 回复「简历模板」获取精选的简历模板
  • 回复「加群」进入500人前端精英群
  • 回复「电子书」下载我整理的大量前端资源,含面试、Vue实战项目、CSS和JavaScript电子书等。
  • 回复「知识点」下载高清JavaScript知识点图谱

每日分享有用的前端开发知识,加我微信:caibaojian89 交流