requestAnimationFrame实现高性能JS动画的原理与兼容方案

作者:像素漫游时间:2026-10-29 13:17:52浏览:343

本文详解requestAnimationFrame相比setTimeout/setInterval在JS动画中的性能优势,包括集中DOM操作、跟随刷新频率及隐藏元素不渲染等特性。文章提供了完整的浏览器兼容封装代码,并对比了mozRequestAnimationFrame与webkitRequestAnimationFrame的实现差异,帮助开发者实现流畅且高效的Web动画。

requestAnimationFrame性能优势概述

利用requestAnimationFrame实现JavaScript动画具有显著的性能优势。接下来简要介绍requestAnimationFrame相较于setTimeout和setInterval的核心优势。

示例一:

requestAnimationFrame相比setTimeout和setInterval的优势主要体现在两点: 1、requestAnimationFrame会将每一帧中的所有DOM操作集中处理,在一次重绘或回流中完成,且重绘或回流的时间间隔紧密跟随浏览器的刷新频率,通常为每秒60帧。 2、当元素隐藏或不可见时,requestAnimationFrame不会进行重绘或回流,从而降低CPU、GPU和内存的占用。 与setTimeout、setInterval类似,requestAnimationFrame也是全局函数。调用它后,浏览器会根据自身频率执行一次重绘,并接收一个回调函数作为参数。在即将开始重绘时,浏览器会调用该回调函数,并传入调用时的时间戳。由于requestAnimationFrame仅执行一次,要实现连续动画效果,必须持续调用它,这与使用setTimeout实现动画的方式类似。requestAnimationFrame返回一个资源标识符,可将其作为参数传入cancelAnimationFrame以取消回调。这与setTimeout的clearTimeout机制相似。 因此,可以将requestAnimationFrame视为专为动画优化的setTimeout版本。不同之处在于,它不由开发者指定回调执行时间,而是跟随浏览器内置的刷新频率执行,从而达到浏览器能实现的最佳动画效果。 目前,支持requestAnimationFrame的浏览器大多仍使用私有前缀实现,因此需要添加前缀。对于不支持该API的浏览器,只能回退使用setTimeout。由于两者使用方式相近,兼容处理并不复杂。对于支持requestAnimationFrame的浏览器,优先使用原生API;不支持的则优雅降级使用setTimeout。封装后即可获得跨浏览器的统一API。 完整代码可参考:https://gist.github.com/chaping/88813f56e75b0fd43f8c


var lastTime = 0;
var prefixes = 'webkit moz ms o'.split(' '); //各浏览器前缀
var requestAnimationFrame = window.requestAnimationFrame;
var cancelAnimationFrame = window.cancelAnimationFrame;
var prefix;
//通过遍历各浏览器前缀,来得到requestAnimationFrame和cancelAnimationFrame在当前浏览器的实现形式
for( var i = 0; i < prefixes.length; i++ ) {
 if ( requestAnimationFrame && cancelAnimationFrame ) {
 break;
 }
 prefix = prefixes[i];
 requestAnimationFrame = requestAnimationFrame || window[ prefix + 'RequestAnimationFrame' ];
 cancelAnimationFrame = cancelAnimationFrame || window[ prefix + 'CancelAnimationFrame' ] || window[ prefix + 'CancelRequestAnimationFrame' ];
}
//如果当前浏览器不支持requestAnimationFrame和cancelAnimationFrame,则会退到setTimeout
if ( !requestAnimationFrame || !cancelAnimationFrame ) {
 requestAnimationFrame = function( callback, element ) {
 var currTime = new Date().getTime();
 //为了使setTimteout的尽可能的接近每秒60帧的效果
 var timeToCall = Math.max( 0, 16 - ( currTime - lastTime ) ); 
 var id = window.setTimeout( function() {
 callback( currTime + timeToCall );
 }, timeToCall );
 lastTime = currTime + timeToCall;
 return id;
 };
 cancelAnimationFrame = function( id ) {
 window.clearTimeout( id );
 };
}
//得到兼容各浏览器的API
window.requestAnimationFrame = requestAnimationFrame; 
window.cancelAnimationFrame = cancelAnimationFrame;

浏览器兼容封装与基础示例

通过上述封装,我们即可在所有浏览器中正常使用requestAnimationFrame和cancelAnimationFrame。 下面通过一个简单示例说明如何运用requestAnimationFrame进行动画,以下代码将实现id为demo的div向右移动至300px的动画效果。


<div id="demo" style="position:absolute; width:100px; height:100px; background:#ccc; left:0; top:0;"></div>
<script>
var demo = document.getElementById('demo');
function rander(){
 demo.style.left = parseInt(demo.style.left) + 1 + 'px'; //每一帧向右移动1px
}
requestAnimationFrame(function(){
 rander();
 //当超过300px后才停止
 if(parseInt(demo.style.left)<=300) requestAnimationFrame(arguments.callee);
});
</script>


示例二:

Web动画演进:从定时器到间隔动画

长期以来,JavaScript动画主要依赖定时器和间隔函数实现。尽管CSS transitions和animations使Web开发中的动画制作更加便捷,但基于JavaScript的动画实现方式多年来变化不大。直到Firefox 4发布,才带来了第一种针对JavaScript动画的改进方法。深入理解这一改进,有助于我们把握Web动画的演进历程。 定时器Timer 创建动画的最初模式是使用链式setTimeout()调用。在Netscape 3流行的时期,开发者们记忆犹新的是网页上常见的固定式最新行情状态栏,其实现方式通常如下:


(function(){ 
 var msg = "新的广告", 
 len = 25, 
 pos = 0, 
 padding = msg.replace(/./g, " ").substr(0,len), 
 finalMsg = padding + msg; 
 function updateText(){ 
 var curMsg = finalMsg.substr(pos++, len); 
 window.status = curMsg; 
 if (pos == finalMsg.length){ pos = 0; } 
 setTimeout(updateText, 100); 
 } 
 setTimeout(updateText, 100); 
})(); 

若要在浏览器中测试此代码,可新建一个 标签来模拟window.status,例如:newsticker example 这种令人困扰的Web模式后来因对window.status禁用而遭到抵制。但随着Explorer 4和Netscape 4的发布,浏览器首次赋予开发者更多页面元素控制权,该技术得以重现。由此产生了通过JavaScript动态改变元素大小、位置、颜色等的全新动画模式。例如,以下代码演示了将div宽度变化为100%的动画(类似进度条):


(function(){ 
 function updateProgress(){ 
 var div = document.getElementByIdx_x("status"); 
 div.style.width = (parseInt(div.style.width, 10) + 5) + "%"; 
 if (div.style.width != "100%"){ setTimeout(updateProgress, 100); } 
 } 
 setTimeout(updateProgress, 100); 
})(); 

尽管动画在页面上的位置不同,但基本原理一致:做出改变,通过setTimeout()间隔更新页面,然后setTimeout再次执行下一次变化,如此循环直至动画完成(参见进度条动画)。早期的状态栏动画采用相同技术,仅动画内容不同。 间隔动画Intervals 随着动画在Web上的成功引入,新的探索随之展开。单个动画已无法满足需求,现在需要处理多个动画。早期的尝试是为每个动画创建独立的动画循环,在早期浏览器中使用setTimeout()创建多个动画较为复杂,因此开发商开始使用setInterval()创建单一的动画循环,以管理页面上所有动画。一个基于setInterval()的基本动画示例如下:


(function(){ 
 function updateAnimations(){ 
 updateText(); 
 updateProgress(); 
 } 
 setInterval(updateAnimations, 100); 
})();

创建一个小型动画库,updateAnimations()方法将循环执行每个动画(同时运行新闻股票和进度条),并进行相应更新。如果没有动画需要更新,该方法可退出而不执行任何操作,甚至停止动画循环,直到有更多动画准备更新。 动画问题中较为棘手的是延迟时间的设定。间隔一方面必须足够短,以确保不同动画流畅运行;另一方面又要足够长,以便浏览器完成渲染。大多数浏览器的刷新频率为60Hz,即每秒60次刷新,且通常不会超过此频率,因为最终用户无法感知更频繁刷新带来的体验提升。 鉴于此,实现流畅动画的最佳时间间隔为1000毫秒/60,约17ms。在此频率下,你将看到流畅的动画,因为你已接近浏览器能达到的最高频率。与之前的动画相比,17ms间隔的动画更加平滑且响应更快(因为动画更新更频繁,未做其他修改)。多个动画可能需要节流,以避免17ms动画完成过快。 问题 尽管基于setInterval()的动画循环比多套使用setTimeout()的循环更高效,但仍存在问题。无论是setInterval()还是setTimeout()都无法保证精确执行时间,你指定的第二个参数仅表示代码何时被添加到浏览器可能执行的UI线程队列中。如果队列中有其他工作在此之前,代码将等待其完成后才执行。简而言之,毫秒级延迟表示的是代码何时被加入队列,而非何时执行。如果UI线程繁忙或正在处理用户操作,代码不会立即执行。 实现平滑动画的关键在于理解下一帧何时被执行,直到目前为止,尚无方法保证下一帧将在浏览器中被绘制。随着基于浏览器的游戏日益流行,开发商对setInterval()和setTimeout()的不精准性感到越来越失望。 浏览器的计时器分辨率加剧了这一问题,计时器对毫秒不精准,以下是一些常见的计时器分辨率: Internet Explorer 8及更早版本:15.625ms Internet Explorer 9及更新版本:4ms。 Firefox和Safari:约10ms。 Chrome计时器:4ms。 IE在版本9之前的分辨率为15.625,因此0~15之间的任意值可能表现为0或15,无明显区别。IE9的计时器分辨率改进为4ms,但在动画场景下仍不精确,Chrome的计时器分辨率为4ms,Firefox和Safari为10ms。因此,即使你将间隔设定为最佳显示效果,得到的也只是近似值。 mozRequestAnimationFrame Mozilla的Robert O'Callahan思考了这一问题,并提出了一个独特的解决方案。他指出CSS transitions和animations的优势在于浏览器知道哪些动画即将发生,因此能获得正确的UI刷新间隔。而对于JavaScript动画,浏览器无法知晓动画正在执行。他的解决方案是创建mozRequestAnimationFrame()方法,告知浏览器哪些JavaScript代码正在执行,从而在浏览器执行部分代码后进行优化。 mozRequestAnimationFrame()方法接受一个参数,该参数是一个在屏幕重绘前被调用的函数。此函数用于生成下合适的DOM样式改变,这些改变应用于下一次重绘。你可以像调用setTimeout()一样链式调用mozRequestAnimationFrame(),例如:


function updateProgress(){ 
 var div = document.getElementByIdx_x("status"); 
 div.style.width = (parseInt(div.style.width, 10) + 5) + "%"; 
 if (div.style.left != "100%"){ 
 mozRequestAnimationFrame(updateProgress); 
 } 
} 

mozRequestAnimationFrame实现细节

mozRequestAnimationFrame(updateProgress); 由于mozRequestAnimationFrame()仅运行给定函数一次,你需要在下次UI动画时再次调用它。同时,你也需要相同的方法来管理何时停止调用。这非常酷,是流畅动画增强的实例。 因此,mozRequestAnimationFrame()解决了浏览器不知道JavaScript动画正在执行以及不知道合适间隔的问题,同时也解决了代码何时被真正执行的问题。 传递给mozRequestAnimationFrame()的函数实际上是一个下一次重绘发生的时间码(以毫秒为单位,自1970年1月1日计算)。这一点至关重要:mozRequestAnimationFrame()实际上列出了将要重绘的点,并告诉你它们所处的时间。这样你就可以决定如何更好地调整动画。 为了获取上次重绘过去的时间,你可以查询mozAnimationStartTime,其中包含过去重绘的时间码。减去传递给回调时的这个值,可以计算出下一次重绘到屏幕所用的时间。使用这些值的典型模式如下:


function draw(timestamp){ 
 //calculate difference since last repaint 
 var diff = timestamp - startTime; 
 //use diff to determine correct next step 
 //reset startTime to this repaint 
 startTime = timestamp; 
 //draw again 
 mozRequestAnimationFrame(draw); 
} 
var startTime = mozAnimationStartTime; 
mozRequestAnimationFrame(draw); 

关键在于,第一次不是通过callback调用时,mozAnimationStartTime是到mozRequestAnimationFrame()经过的时间。如果在回调函数中,mozAnimationStartTime是通过参数传递进来的时间码平均值。 webkitRequestAnimationFrame 在许多人热衷于Chrome时,随即创建了webkitRequestAnimationFrame()方法。这个版本与Firefox版本在两方面存在细微差异。一方面,它不通过回调函数传递时间码,你将无法知道下次重绘何时发生;另一方面,它添加了第二个可选参数,用于确定哪一个DOM元素发生改变。因此,如果你知道重绘发生在页面哪个部分的元素内,你可以限制重绘发生的区域。 应该不会感到惊讶,没有相应的mozAnimationStartTime,因为如果没有下一个重绘的时间信息,这并非很有益。有,只是webkitCancelAnimationFrame()取消了之前计划的重绘。 如果你不需要精确的时间差异,可以使用以下方式来创建用于Firefox4和Chrome10+的动画:


function draw(timestamp){ 
 //calculate difference since last repaint 
 var drawStart = (timestamp || Date.now()), 
 diff = drawStart - startTime; 
 //use diff to determine correct next step 
 //reset startTime to this repaint 
 startTime = drawStart; 
 //draw again 
 requestAnimationFrame(draw); 
} 
var requestAnimationFrame = window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame, 
startTime = window.mozAnimationStartTime || Date.now(); 
requestAnimationFrame(draw); 

webkitRequestAnimationFrame与总结

这种模式使用可用方法创建以花费多少时间为理念的循环动画。Firefox使用时间代码信息是有用的,而Chrome默认为欠精准的时间对象。当使用这种模式时,时间的差异给你一种多少时间过去了的想法,但不会告诉你Chrome的下一次重绘出现在何时。不过这比只有多少时间过去了的模糊概念要好些。 总结 mozRequestAnimationFrame()方法的介绍为推动JavaScript动画及Web的历史发展有着非常重要的作用。如前所述,JavaScript动画的状态几乎与JavaScript初期一样。随着浏览器逐渐推出CSS transitions和animations,很高兴看到基于JavaScript的动画的关注,因为这些在基于Web的游戏领域将变得更重要且更与CPU联系紧密。知道JavaScript何时尝试动画,允许浏览器做更多的优化处理,包括在tab处于后台或移动设备电量过低时停止进程。 该requestAnimationFrame()API现在正由W3C起草新议案,并正由Mozilla和Google努力使之成为Web大舞台的一部分。很高兴能看到这两大集团这么迅速的兼容(可能不完全)实现。 RequestAnimFrame使用 对于一个侦中对DOM的所有操作,只进行一次Layout和Paint。 如果发生动画的元素被隐藏了,那么就不再去Paint。


window.requestAnimFrame = (function(){ 
 return window.requestAnimationFrame || 
 window.webkitRequestAnimationFrame || 
 window.mozRequestAnimationFrame || 
 window.oRequestAnimationFrame || 
 window.msRequestAnimationFrame || 
 function( callback ){ 
  window.setTimeout(callback, 1000/60); 
 }; 
})(); 
//调用 
function animationLoop(elem){ 
 requestAnimFrame(animationLoop); 
//logic 
} 
Or
window.requestAnimFrame = (function(w, r) { 
 w['r'+r] = w['r'+r] || w['webkitR'+r] || w['mozR'+r] || w['msR'+r] || w['oR'+r] || function(c){ w.setTimeout(c, 1000 / 60); }; 
 return w['r'+r]; 
})(window, 'equestAnimationFrame'); 

以上通过两段代码示例详解说明了使用requestAnimationFrame实现JS动画性能好的原理及方法,希望对大家有帮助。

作者最新文章