使用requestAnimationFrame进行性能优化

上周在看了layzr.js这个小而精的懒加载插件源码,代码量不超过200行,无任何依赖,确实经典,其中印象比较深的是使用了requestAnimationFrame这个JS API延迟代码执行,优化性能。

Layzr.prototype._requestTick = function() {
  if(!this._ticking) {
    requestAnimationFrame(this.update.bind(this));
    this._ticking = true;
  }
};

首先介绍下requestAnimationFrame:

requestAnimationFrame()用来在页面重绘前通知浏览器调用一个指定函数,以此满足开发者操作动画的需求。

在以往开发中,一般会使用setTimeout和setInterval实现动画,如果需要绘制60fps的动画,则需每16.7毫秒(1000/60)绘制一帧,然而由于浏览器对setTimeout的延时计算并不精确(原因在于:首先浏览器内置时钟的更新频率并不一致,其次setTimeout中的回调在延时后并不会立即执行,而是被加入执行队列中以此执行),因此造成掉帧现象。其次还有显示器刷新频率问题、渲染画面时间问题等一系列问题。因此浏览器厂商提供了requestAnimationFrame(简称rAF)进行动画操作,这个Native API把我们从纠结于多久刷新的一次的困境中解救出来(其实rAF也不关心距离下次屏幕刷新页面还需要多久)。当我们调用这个函数的时候,我们告诉它需要做两件事: 1. 我们需要新的一帧;2.当你渲染新的一帧时需要执行我传给你的回调函数。那么它解决了我们上面描述的第一个问题,产生新的一帧的时机。

requestAnimationFrame的应用

我们在页面动画中常见的需求是获取元素的宽度、设置颜色等,我们一般会这样写:

var div = document.getElementById("foo");
var currentWidth = div.innerWidth;
div.style.backgroundColor = "blue";

// 更多代码。。。

如代码,我们获取了div的宽度、并设置其背景颜色,然而这样写有两处不足:

1、严重的性能问题:js在请求innerWidth等属性时,会主动触发浏览器reflow(更多触发reflow的操作请看这篇文章,由于reflow会消耗大量性能,因此浏览器一般会攒着一批样式,等待时机一次性reflow以便节省性能。而代码中并没有迫切需要知道innerWidth的情况下,应该尽量延迟它;

2、更新背景颜色的代码过于提前:实际上,虽然js代码按顺序执行,但由于js是单线程的,浏览器必须等js运行完毕才会调用UI线程更新UI变化。

综上,我们需要使用rAF推迟代码执行,在浏览器自动reflow时触发它:

requestAnimationFrame(function(){
    var el = document.getElementById("foo");
    var currentWidth = el.innerWidth;
    el.style.backgroundColor = "blue";
});

requestAnimationFrame在layzr.js中的妙用

回到文章最上面,layzr.js在懒加载需要监听scroll事件。我们知道scroll事件会被频繁触发,如果把太多代码放在这类事件回调中执行会遇到严重的性能问题,因此,我们一般会把回调放在一个timer中,有间隔的检测是否滚动。比如这样:

var didScroll = false;
$(window).scroll(function() {
    didScroll = true;
});
setInterval(function() {
    if ( didScroll ) {
        didScroll = false;
} }, 250)

那么layzr.js是怎么使用rAF完成相同功能的呢?

首先它定义了一个属性用于记录滚动情况:

this._lastScroll = 0;

在事件回调中对这个属性重新赋值:

Layzr.prototype._requestScroll = function() {
    if(this._optionsContainer === window) {
            this._lastScroll = window.pageYOffset;
    } else {
            this._lastScroll = this._optionsContainer.scrollTop +       this._getOffset(this._optionsContainer);
  }

  this._requestTick();
};

然后将分离出来的回调全部放入一个update函数中:

Layzr.prototype.update = function() {
  // cache nodelist length
  var nodesLength = this._nodes.length;

  // loop through nodes
  for(var i = 0; i < nodesLength; i++) {
    // cache node
    var node = this._nodes[i];

    // check if node has mandatory attribute
    if(node.hasAttribute(this._optionsAttr)) {
      // check if node in viewport
      if(this._inViewport(node)) {
        // reveal node
        this._reveal(node);
      }
    }
  }

最后将rAF操作放入_requestTich函数中将其解耦:

Layzr.prototype._requestTick = function() {
  if(!this._ticking) {
    requestAnimationFrame(this.update.bind(this));
    this._ticking = true;
  }
};