requestAnimationFrame vs. setTimeout: 优化动画性能,避免不必要的重绘
大家好,今天我们来深入探讨一下 requestAnimationFrame
(rAF) 和 setTimeout
在动画实现上的差异,以及为什么 rAF 通常是更好的选择,尤其是在优化动画性能和避免不必要的重绘方面。
setTimeout 的运作方式及潜在问题
setTimeout
是 JavaScript 中一个常用的定时器函数,它允许我们在指定的时间延迟后执行一段代码。在动画实现中,我们经常使用 setTimeout
来周期性地更新元素的位置、大小、颜色等属性,从而产生动画效果。
例如,以下代码使用 setTimeout
实现一个简单的移动动画:
let element = document.getElementById('myElement');
let position = 0;
function animate() {
position += 1;
element.style.left = position + 'px';
setTimeout(animate, 16); // 大约 60 FPS
}
animate();
这段代码每隔 16 毫秒更新一次元素的位置,理论上可以达到 60 FPS 的动画效果。然而,这种实现方式存在一些潜在问题:
-
时间精度问题:
setTimeout
的回调函数何时执行,并不能保证精确到指定的延迟时间。浏览器的 JavaScript 引擎需要处理其他任务,例如事件处理、垃圾回收等,这些任务可能会延迟setTimeout
回调函数的执行。 实际上setTimeout
的延迟时间只是一个最小值,真正执行的时间可能会晚于设定的值。 -
页面不可见时的资源浪费: 即使页面被隐藏(例如切换到其他标签页),
setTimeout
仍然会继续执行,这会导致不必要的资源浪费,因为动画更新不会被用户看到。浏览器无法得知动画是否可见,依然会继续执行setTimeout
回调中的代码。 -
可能导致卡顿: 如果
setTimeout
的回调函数执行时间过长,可能会导致页面卡顿,影响用户体验。 例如,如果动画逻辑比较复杂,或者浏览器性能较低,setTimeout
回调函数执行的时间可能超过 16 毫秒,这会导致动画帧率下降,产生卡顿感。 -
不同浏览器的差异: 不同浏览器对
setTimeout
的实现可能存在差异,这会导致在不同浏览器上动画效果不一致。
requestAnimationFrame 的优势:与浏览器渲染周期同步
requestAnimationFrame
是一个专门用于优化动画的 API。与 setTimeout
不同,requestAnimationFrame
会在浏览器下一次重绘之前执行回调函数。这意味着 rAF 的回调函数会在浏览器准备更新屏幕显示内容时被调用,从而实现与浏览器渲染周期的同步。
使用 requestAnimationFrame
实现与上面相同的移动动画:
let element = document.getElementById('myElement');
let position = 0;
function animate() {
position += 1;
element.style.left = position + 'px';
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
这段代码与 setTimeout
的版本类似,但使用了 requestAnimationFrame
来调度动画更新。rAF 的主要优势在于:
-
与浏览器渲染周期同步: rAF 的回调函数会在浏览器下一次重绘之前执行,这确保了动画更新与浏览器渲染周期同步,从而避免了不必要的重绘,提高了动画性能。 浏览器会根据设备的刷新率来决定何时执行 rAF 的回调函数,通常是每秒 60 次或更高。
-
页面不可见时的优化: 当页面被隐藏时,浏览器会自动暂停 rAF 的回调函数,从而节省资源。 当用户切换到其他标签页或最小化浏览器窗口时,浏览器会停止执行 rAF 回调函数,直到页面重新可见时才会恢复。
-
更高的动画性能: 由于 rAF 与浏览器渲染周期同步,并且能够智能地管理动画更新,因此可以实现更高的动画性能,减少卡顿现象。 rAF 允许浏览器优化动画的执行,例如合并多个动画帧,或者在硬件加速的支持下进行动画渲染。
-
更流畅的动画: 浏览器会根据当前系统的负载情况,动态调整 rAF 回调函数的执行频率,从而保证动画的流畅性。
深入理解浏览器渲染周期
要理解 requestAnimationFrame
的优势,需要先了解浏览器的渲染周期。一个典型的浏览器渲染周期包含以下几个步骤:
- 解析 HTML: 浏览器解析 HTML 文档,构建 DOM 树。
- 解析 CSS: 浏览器解析 CSS 样式,构建 CSSOM 树。
- 构建渲染树: 浏览器将 DOM 树和 CSSOM 树合并,构建渲染树。渲染树只包含需要渲染的节点,例如可见的元素和文本。
- 布局(Layout): 浏览器计算渲染树中每个节点的位置和大小。
- 绘制(Paint): 浏览器将渲染树中的每个节点绘制到屏幕上。
- 合成(Composite): 浏览器将多个图层合成为最终的图像,显示在屏幕上。
requestAnimationFrame
的回调函数会在布局和绘制之前执行。这意味着我们可以在 rAF 的回调函数中修改 DOM 元素的样式,然后浏览器会在下一次重绘时将这些修改应用到屏幕上。
渲染阶段 | 描述 | requestAnimationFrame 介入点 |
---|---|---|
解析 HTML | 构建 DOM 树 | 无 |
解析 CSS | 构建 CSSOM 树 | 无 |
构建渲染树 | 合并 DOM 树和 CSSOM 树 | 无 |
布局 | 计算渲染树中每个节点的位置和大小 | rAF 回调函数可以在这里修改 DOM 样式 |
绘制 | 将渲染树中的每个节点绘制到屏幕上 | 浏览器根据修改后的样式进行绘制 |
合成 | 将多个图层合成为最终的图像,显示在屏幕上 | 无 |
避免不必要的重绘:性能优化的关键
重绘(Repaint)是指浏览器重新绘制屏幕的一部分或全部内容。重绘是一个消耗资源的操作,频繁的重绘会导致页面卡顿,影响用户体验。
以下是一些常见的导致重绘的操作:
- 修改 DOM 元素的样式(例如颜色、背景、字体等)
- 修改 DOM 元素的几何属性(例如位置、大小、边距等)
- 添加或删除 DOM 元素
- 修改页面内容
- 滚动页面
- 触发伪类(例如
:hover
)
requestAnimationFrame
可以帮助我们避免不必要的重绘,从而优化动画性能。通过将动画更新放在 rAF 的回调函数中,我们可以确保动画更新与浏览器渲染周期同步,避免在同一帧中进行多次重绘。
例如,以下代码使用 setTimeout
在同一帧中多次修改 DOM 元素的样式:
let element = document.getElementById('myElement');
setTimeout(() => {
element.style.color = 'red';
element.style.backgroundColor = 'blue';
element.style.fontSize = '20px';
}, 0);
这段代码会在很短的时间内连续修改元素的颜色、背景和字体大小。由于 setTimeout
的延迟时间为 0,这些修改可能会在同一帧中进行,导致浏览器进行多次重绘。
使用 requestAnimationFrame
可以避免这种情况:
let element = document.getElementById('myElement');
requestAnimationFrame(() => {
element.style.color = 'red';
element.style.backgroundColor = 'blue';
element.style.fontSize = '20px';
});
这段代码将所有样式修改放在 rAF 的回调函数中。浏览器会在下一次重绘之前执行 rAF 的回调函数,然后将所有样式修改一次性应用到屏幕上,从而避免了多次重绘。
如何选择 requestAnimationFrame 和 setTimeout?
通常情况下,requestAnimationFrame
是动画实现的更好选择。它与浏览器渲染周期同步,能够优化动画性能,避免不必要的重绘,并且在页面不可见时自动暂停,节省资源。
然而,在某些特殊情况下,setTimeout
仍然有用武之地:
- 不需要与浏览器渲染周期同步的任务: 如果我们需要执行一些不需要与浏览器渲染周期同步的任务,例如发送统计数据、执行一些计算等,
setTimeout
是一个合适的选择。 - 兼容性问题: 虽然
requestAnimationFrame
已经被大多数现代浏览器支持,但在一些老旧的浏览器上可能不支持。在这种情况下,我们可以使用setTimeout
作为备选方案。
以下是一些选择 requestAnimationFrame
和 setTimeout
的建议:
使用场景 | 推荐选择 | 原因 |
---|---|---|
实现动画效果 | requestAnimationFrame |
与浏览器渲染周期同步,优化动画性能,避免不必要的重绘,页面不可见时自动暂停。 |
不需要与浏览器渲染周期同步的任务 | setTimeout |
可以独立于浏览器渲染周期执行任务。 |
需要兼容老旧浏览器 | setTimeout (配合 polyfill) |
老旧浏览器可能不支持 requestAnimationFrame ,可以使用 setTimeout 作为备选方案。 可以使用 polyfill 来提供 requestAnimationFrame 的兼容性支持。 |
需要在指定的时间间隔后执行任务(例如轮询) | setTimeout |
可以设置固定的时间间隔来执行任务。 |
需要精确控制任务的执行时间(尽管通常无法精确控制) | setTimeout |
尽管 setTimeout 的执行时间并不精确,但在某些情况下,我们可能需要尽可能地控制任务的执行时间。 这种情况需要注意,setTimeout 的延迟只是一个最小值,真正执行的时间可能会晚于设定的值,并且受到其他任务的干扰。 |
requestAnimationFrame 的 Polyfill
为了在不支持 requestAnimationFrame
的老旧浏览器上使用 rAF,我们可以使用 Polyfill。一个简单的 rAF Polyfill 如下所示:
(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);
};
}
}());
这段代码首先检查浏览器是否支持 webkitRequestAnimationFrame
或 mozRequestAnimationFrame
。如果支持,则使用这些 API。否则,使用 setTimeout
来模拟 requestAnimationFrame
的行为。
使用 requestAnimationFrame 进行更高级的动画控制
除了简单的移动动画,requestAnimationFrame
还可以用于实现更高级的动画效果,例如缓动动画、物理动画等。
缓动动画: 缓动动画是指动画的速度不是恒定的,而是随着时间的推移逐渐变化的。缓动动画可以使动画效果更加自然流畅。
以下代码使用 requestAnimationFrame
实现一个简单的缓动动画:
let element = document.getElementById('myElement');
let startPosition = 0;
let endPosition = 200;
let duration = 1000; // 动画持续时间,单位毫秒
let startTime;
function animate(currentTime) {
if (!startTime) startTime = currentTime;
let timeElapsed = currentTime - startTime;
let progress = timeElapsed / duration; // 动画进度,范围 0-1
// 使用缓动函数,例如 ease-in-out
progress = Math.max(0, Math.min(1, progress)); // 确保 progress 在 0-1 之间
progress = progress * progress * (3 - 2 * progress); // ease-in-out 缓动函数
let position = startPosition + (endPosition - startPosition) * progress;
element.style.left = position + 'px';
if (timeElapsed < duration) {
requestAnimationFrame(animate);
} else {
// 动画结束
element.style.left = endPosition + 'px';
}
}
requestAnimationFrame(animate);
这段代码使用一个 ease-in-out 缓动函数来计算动画的进度,从而实现缓动动画效果。 ease-in-out 缓动函数的公式为 progress * progress * (3 - 2 * progress)
。
物理动画: 物理动画是指基于物理原理(例如重力、摩擦力、弹力等)的动画。物理动画可以使动画效果更加真实自然。
以下代码使用 requestAnimationFrame
实现一个简单的弹跳动画:
let element = document.getElementById('myElement');
let position = 0;
let velocity = 0;
let gravity = 0.5;
let bounce = 0.8;
function animate() {
velocity += gravity;
position += velocity;
if (position > 200) {
position = 200;
velocity *= -bounce;
}
element.style.top = position + 'px';
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
这段代码模拟了重力和弹跳效果,从而实现弹跳动画。
总结:选择合适的工具,优化动画性能
requestAnimationFrame
和 setTimeout
都是 JavaScript 中常用的定时器函数,但它们在动画实现上的行为和性能表现存在差异。requestAnimationFrame
与浏览器渲染周期同步,能够优化动画性能,避免不必要的重绘,并且在页面不可见时自动暂停,节省资源。因此,在大多数情况下,requestAnimationFrame
是动画实现的更好选择。但是 setTimeout
在一些特定场景下依然具有其用途。在选择使用哪个API时,需要根据具体的应用场景进行权衡,选择最适合的工具。