使用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;
}
}; 