流畅如丝:利用requestAnimationFrame优化你的Web动画体验
requestAnimationFrame
是前端开发中用于优化动画性能的 API。它允许浏览器在下一次重绘之前执行指定的回调函数,通常用于实现平滑的动画效果。
1.作用
-
优化性能:
requestAnimationFrame
会根据浏览器的刷新率(通常是 60Hz,即每秒 60 帧)来调用回调函数,确保动画流畅且不浪费资源。 -
节省资源:当页面不可见或最小化时,浏览器会自动暂停
requestAnimationFrame
的执行,减少 CPU 和 GPU 的消耗。 -
避免丢帧:与
setTimeout
或setInterval
相比,requestAnimationFrame
能更好地与浏览器的渲染周期同步,减少丢帧现象。
2.使用方法
-
基本用法
function animate() { // 动画逻辑 requestAnimationFrame(animate); } requestAnimationFrame(animate);
-
停止动画:
let animationId; function animate() { // 动画逻辑 animationId = requestAnimationFrame(animate); } animate(); // 停止动画 cancelAnimationFrame(animationId);
优势对比
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>requestAnimationFrame vs setTimeout - 点击开始</title>
<style>
.box {
width: 50px;
height: 50px;
position: relative;
top: 0;
left: 0;
margin-bottom: 20px; /* 增加间距 */
}
#rafBox {
background-color: red;
}
#timeoutBox {
background-color: blue;
}
.label {
font-family: Arial, sans-serif;
margin-bottom: 10px;
}
#startButton {
padding: 10px 20px;
font-size: 16px;
margin-bottom: 20px;
cursor: pointer;
}
</style>
</head>
<body>
<button id="startButton">开始动画</button>
<div>
<div class="label">requestAnimationFrame</div>
<div id="rafBox" class="box"></div>
</div>
<div>
<div class="label">setTimeout</div>
<div id="timeoutBox" class="box"></div>
</div>
<script>
// 增加动画复杂度:模拟一些计算任务
function heavyTask() {
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += Math.random();
}
}
// requestAnimationFrame 动画
const rafBox = document.getElementById('rafBox');
let rafPosition = 0;
let rafStartTime;
let rafFrameCount = 0;
function rafAnimate() {
heavyTask(); // 模拟复杂计算
rafPosition += 2; // 每次移动 2px
rafBox.style.left = rafPosition + 'px';
rafFrameCount++;
if (rafPosition < 600) { // 移动到 600px 时停止
requestAnimationFrame(rafAnimate);
} else {
const rafEndTime = performance.now();
console.log(`requestAnimationFrame 完成!用时:${(rafEndTime - rafStartTime).toFixed(2)}ms,帧数:${rafFrameCount}`);
}
}
// setTimeout 动画
const timeoutBox = document.getElementById('timeoutBox');
let timeoutPosition = 0;
let timeoutStartTime;
let timeoutFrameCount = 0;
function timeoutAnimate() {
heavyTask(); // 模拟复杂计算
timeoutPosition += 2; // 每次移动 2px
timeoutBox.style.left = timeoutPosition + 'px';
timeoutFrameCount++;
if (timeoutPosition < 600) { // 移动到 600px 时停止
setTimeout(timeoutAnimate, 16); // 模拟 60Hz 刷新率
} else {
const timeoutEndTime = performance.now();
console.log(`setTimeout 完成!用时:${(timeoutEndTime - timeoutStartTime).toFixed(2)}ms,帧数:${timeoutFrameCount}`);
}
}
// 点击按钮后启动动画
const startButton = document.getElementById('startButton');
startButton.addEventListener('click', () => {
// 重置方块位置
rafBox.style.left = '0px';
timeoutBox.style.left = '0px';
rafPosition = 0;
timeoutPosition = 0;
// 记录开始时间
rafStartTime = performance.now();
timeoutStartTime = performance.now();
// 启动动画
rafAnimate();
timeoutAnimate();
// 禁用按钮,防止重复点击
startButton.disabled = true;
});
</script>
</body>
</html>
-
你会看到一个“开始动画”按钮,点击按钮后:
-
红色方块:使用
requestAnimationFrame
,动画更加流畅。 -
蓝色方块:使用
setTimeout
,动画可能会出现卡顿。
-
打开浏览器的控制台(按 F12
),可以看到两者的完成时间和帧数对比
requestAnimationFrame
的兼容封装
requestAnimationFrame
在不同浏览器中的兼容性确实存在差异,尤其是在一些旧版本的浏览器中(如 IE9 及以下)。为了确保代码的兼容性,我们可以封装一个通用的 requestAnimationFrame
方法,如果浏览器不支持 requestAnimationFrame
,则自动降级为 setTimeout
// 兼容性封装
const requestAnimFrame = (function () {
return (
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback) {
// 如果不支持 requestAnimationFrame,则使用 setTimeout 模拟
return window.setTimeout(callback, 1000 / 60); // 模拟 60Hz 刷新率
}
);
})();
// 兼容性封装 cancelAnimationFrame
const cancelAnimFrame = (function () {
return (
window.cancelAnimationFrame ||
window.webkitCancelAnimationFrame ||
window.mozCancelAnimationFrame ||
window.oCancelAnimationFrame ||
window.msCancelAnimationFrame ||
function (id) {
// 如果不支持 cancelAnimationFrame,则使用 clearTimeout
window.clearTimeout(id);
}
);
})();
3.使用方法
启动动画
let animationId;
function animate() {
// 动画逻辑
animationId = requestAnimFrame(animate);
}
animate(); // 启动动画
停止动画
cancelAnimFrame(animationId); // 停止动画
4.完整示例
以下是一个完整的示例,展示如何使用封装的 requestAnimFrame
方法,并且保留啦优势对比的蓝色方块,同时也增加了两个按钮,可以重复观看,以感受他们的差异!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>requestAnimationFrame vs setTimeout 对比</title>
<style>
.box {
width: 50px;
height: 50px;
position: relative;
top: 0;
left: 0;
margin-bottom: 20px; /* 增加间距 */
}
#rafBox {
background-color: red;
}
#timeoutBox {
background-color: blue;
}
.label {
font-family: Arial, sans-serif;
margin-bottom: 10px;
}
button {
padding: 10px 20px;
font-size: 16px;
margin-right: 10px;
cursor: pointer;
}
</style>
</head>
<body>
<button id="startButton">开始动画</button>
<button id="resetButton">重新执行动画</button>
<div>
<div class="label">requestAnimationFrame</div>
<div id="rafBox" class="box"></div>
</div>
<div>
<div class="label">setTimeout</div>
<div id="timeoutBox" class="box"></div>
</div>
<script>
// 兼容性封装
const requestAnimFrame = (function () {
return (
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback) {
// 如果不支持 requestAnimationFrame,则使用 setTimeout 模拟
return window.setTimeout(callback, 1000 / 60); // 模拟 60Hz 刷新率
}
);
})();
const cancelAnimFrame = (function () {
return (
window.cancelAnimationFrame ||
window.webkitCancelAnimationFrame ||
window.mozCancelAnimationFrame ||
window.oCancelAnimationFrame ||
window.msCancelAnimationFrame ||
function (id) {
// 如果不支持 cancelAnimationFrame,则使用 clearTimeout
window.clearTimeout(id);
}
);
})();
// requestAnimationFrame 动画
const rafBox = document.getElementById('rafBox');
let rafPosition = 0;
let rafAnimationId;
function rafAnimate() {
rafPosition += 2; // 每次移动 2px
rafBox.style.left = rafPosition + 'px';
if (rafPosition < 600) { // 移动到 600px 时停止
rafAnimationId = requestAnimFrame(rafAnimate);
}
}
// setTimeout 动画
const timeoutBox = document.getElementById('timeoutBox');
let timeoutPosition = 0;
let timeoutAnimationId;
function timeoutAnimate() {
timeoutPosition += 2; // 每次移动 2px
timeoutBox.style.left = timeoutPosition + 'px';
if (timeoutPosition < 600) { // 移动到 600px 时停止
timeoutAnimationId = setTimeout(timeoutAnimate, 16); // 模拟 60Hz 刷新率
}
}
// 点击按钮启动动画
document.getElementById('startButton').addEventListener('click', () => {
// 启动 requestAnimationFrame 动画
rafAnimate();
// 启动 setTimeout 动画
timeoutAnimate();
});
// 点击按钮重新执行动画
document.getElementById('resetButton').addEventListener('click', () => {
// 停止当前动画
cancelAnimFrame(rafAnimationId);
clearTimeout(timeoutAnimationId);
// 重置方块位置
rafBox.style.left = '0px';
timeoutBox.style.left = '0px';
rafPosition = 0;
timeoutPosition = 0;
// 重新启动动画
rafAnimate();
timeoutAnimate();
});
</script>
</body>
</html>
5.代码说明
-
兼容性封装:
-
requestAnimFrame
和cancelAnimFrame
封装了requestAnimationFrame
和cancelAnimationFrame
的兼容性逻辑。
-
-
动画逻辑:
-
红色方块:使用
requestAnimFrame
,动画更加流畅。 -
蓝色方块:使用
setTimeout
,动画可能会出现卡顿。
-
-
按钮功能:
-
开始动画:点击后同时启动
requestAnimFrame
和setTimeout
的动画。 -
重新执行动画:点击后停止当前动画,重置方块位置,并重新启动动画。
-
6.效果展示
7.案例总结
通过这个示例,你可以直观地看到 requestAnimFrame
和 setTimeout
的差异:
-
requestAnimFrame
:动画流畅,性能更优。 -
setTimeout
:动画可能会出现卡顿。 -
备注:网上随便下载的gif录屏软件,貌似这种差异不是很明显,建议自己复制代码查看,我实际看到的还是很明显的
8.性能优化建议
-
setTimeout
的性能问题:-
丢帧:
setTimeout
的时间间隔是固定的(如 16ms 模拟 60Hz),但无法保证与浏览器的渲染周期同步,可能导致丢帧或卡顿。 -
资源浪费:即使页面不可见或最小化,
setTimeout
仍会继续运行,浪费 CPU 和 GPU 资源。 -
精度问题:
setTimeout
的时间精度受系统负载影响,可能导致动画不流畅。
-
-
requestAnimationFrame
的性能优势:-
与浏览器渲染同步:
requestAnimationFrame
会在每次浏览器重绘前调用回调函数,确保动画流畅。 -
节省资源:当页面不可见或最小化时,
requestAnimationFrame
会自动暂停,减少资源消耗。 -
高精度:
requestAnimationFrame
的时间戳精度更高,适合高性能动画。
-
9.注意事项
-
避免频繁操作 DOM:
-
在动画回调函数中,尽量减少对 DOM 的频繁操作(如修改样式或布局),因为 DOM 操作会触发重排(reflow)和重绘(repaint),影响性能。
-
可以通过以下方式优化:
-
使用
transform
和opacity
代替top
、left
等属性,因为前者不会触发重排。 -
使用
will-change
属性提示浏览器优化渲染。
-
-
-
避免阻塞主线程:
-
如果动画回调函数中有复杂的计算任务(如大量循环或递归),可能会导致主线程阻塞,影响动画流畅性。
-
可以通过以下方式优化:
-
将复杂计算任务放到 Web Worker 中执行。
-
使用
requestIdleCallback
处理低优先级的任务。
-
-
-
处理动画停止:
-
使用
cancelAnimationFrame
或clearTimeout
及时停止动画,避免不必要的资源消耗。 -
在页面不可见时(如切换标签页),可以通过
Page Visibility API
检测页面状态,暂停动画。
-
-
兼容性问题:
-
虽然
requestAnimationFrame
在现代浏览器中支持良好,但在旧版本浏览器(如 IE9 及以下)中需要降级为setTimeout
。 -
使用兼容性封装(如前面的代码)可以解决这个问题。
-
-
动画帧率控制:
-
requestAnimationFrame
的帧率通常是 60Hz,但如果动画逻辑过于复杂,可能会导致帧率下降。 -
可以通过时间戳(
performance.now()
)计算帧间隔,动态调整动画逻辑,确保帧率稳定。
-
-
内存泄漏:
-
如果动画回调函数中引用了外部变量或 DOM 元素,可能会导致内存泄漏。
-
确保在动画停止时,清理不必要的引用。
-
10.优化建议
-
使用硬件加速:
-
通过
transform: translate3d(0, 0, 0)
或will-change: transform
启用 GPU 加速,提升动画性能。
-
-
减少重排和重绘:
-
使用
transform
和opacity
实现动画,避免修改width
、height
、top
、left
等属性。
-
-
批量操作 DOM:
-
如果需要修改多个 DOM 元素的样式,可以使用
DocumentFragment
或requestAnimationFrame
批量处理,减少重排次数。
-
-
使用 Web Worker:
-
将复杂的计算任务放到 Web Worker 中执行,避免阻塞主线程。
-
-
性能监控:
-
使用
performance.now()
或浏览器开发者工具(如 Chrome 的 Performance 面板)监控动画性能,找出性能瓶颈。
-
11.优化后的动画
以下是一个优化后的动画示例,结合了上述建议:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>三个方块的性能对比</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
.animation-container {
margin-bottom: 40px; /* 每个动画区块之间的间距 */
}
.label {
font-size: 16px;
margin-bottom: 10px; /* 文字与方块的间距 */
}
.box {
width: 50px;
height: 50px;
position: relative;
left: 0;
}
#optimizedBox {
background-color: red;
will-change: transform; /* 启用硬件加速 */
}
#unoptimizedRAFBox {
background-color: green;
}
#unoptimizedTimeoutBox {
background-color: blue;
}
button {
padding: 10px 20px;
font-size: 16px;
margin-right: 10px;
cursor: pointer;
}
</style>
</head>
<body>
<button id="startButton">开始动画</button>
<button id="resetButton">重新开始动画</button>
<!-- 优化后的动画 -->
<div class="animation-container">
<div class="label">优化后的动画(requestAnimationFrame + transform)</div>
<div id="optimizedBox" class="box"></div>
</div>
<!-- 未优化的 requestAnimationFrame 动画 -->
<div class="animation-container">
<div class="label">未优化的 requestAnimationFrame 动画(使用 left)</div>
<div id="unoptimizedRAFBox" class="box"></div>
</div>
<!-- 未优化的 setTimeout 动画 -->
<div class="animation-container">
<div class="label">未优化的 setTimeout 动画(使用 left)</div>
<div id="unoptimizedTimeoutBox" class="box"></div>
</div>
<script>
// 兼容性封装
const requestAnimFrame = (function () {
return (
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback) {
return window.setTimeout(callback, 1000 / 60);
}
);
})();
const cancelAnimFrame = (function () {
return (
window.cancelAnimationFrame ||
window.webkitCancelAnimationFrame ||
window.mozCancelAnimationFrame ||
window.oCancelAnimationFrame ||
window.msCancelAnimationFrame ||
function (id) {
window.clearTimeout(id);
}
);
})();
// 优化后的动画(requestAnimationFrame + transform)
const optimizedBox = document.getElementById('optimizedBox');
let optimizedPosition = 0;
let optimizedAnimationId;
function optimizedAnimate() {
optimizedPosition += 2; // 每次移动 2px
optimizedBox.style.transform = `translateX(${optimizedPosition}px)`; // 使用 transform
if (optimizedPosition < 600) { // 移动到 600px 时停止
optimizedAnimationId = requestAnimFrame(optimizedAnimate);
}
}
// 未优化的 requestAnimationFrame 动画(使用 left)
const unoptimizedRAFBox = document.getElementById('unoptimizedRAFBox');
let unoptimizedRAFPosition = 0;
let unoptimizedRAFAnimationId;
function unoptimizedRAFAnimate() {
unoptimizedRAFPosition += 2; // 每次移动 2px
unoptimizedRAFBox.style.left = unoptimizedRAFPosition + 'px'; // 使用 left
if (unoptimizedRAFPosition < 600) { // 移动到 600px 时停止
unoptimizedRAFAnimationId = requestAnimFrame(unoptimizedRAFAnimate);
}
}
// 未优化的 setTimeout 动画(使用 left)
const unoptimizedTimeoutBox = document.getElementById('unoptimizedTimeoutBox');
let unoptimizedTimeoutPosition = 0;
let unoptimizedTimeoutAnimationId;
function unoptimizedTimeoutAnimate() {
unoptimizedTimeoutPosition += 2; // 每次移动 2px
unoptimizedTimeoutBox.style.left = unoptimizedTimeoutPosition + 'px'; // 使用 left
if (unoptimizedTimeoutPosition < 600) { // 移动到 600px 时停止
unoptimizedTimeoutAnimationId = setTimeout(unoptimizedTimeoutAnimate, 16); // 模拟 60Hz 刷新率
}
}
// 点击按钮启动动画
document.getElementById('startButton').addEventListener('click', () => {
// 启动优化后的动画
optimizedAnimate();
// 启动未优化的 requestAnimationFrame 动画
unoptimizedRAFAnimate();
// 启动未优化的 setTimeout 动画
unoptimizedTimeoutAnimate();
});
// 点击按钮重新开始动画
document.getElementById('resetButton').addEventListener('click', () => {
// 停止当前动画
cancelAnimFrame(optimizedAnimationId);
cancelAnimFrame(unoptimizedRAFAnimationId);
clearTimeout(unoptimizedTimeoutAnimationId);
// 重置方块位置
optimizedBox.style.transform = 'translateX(0px)';
unoptimizedRAFBox.style.left = '0px';
unoptimizedTimeoutBox.style.left = '0px';
optimizedPosition = 0;
unoptimizedRAFPosition = 0;
unoptimizedTimeoutPosition = 0;
// 重新启动动画
optimizedAnimate();
unoptimizedRAFAnimate();
unoptimizedTimeoutAnimate();
});
</script>
</body>
</html>
12.效果展示
csdn上gif大小不能超过5M,我这动图超过5M啦,在线压缩居然要收费,算了!
大家自己复制源码感受一下吧!不贴gif图啦,jpg的对付着看吧!!!
你可以清晰地看到三种实现方式的性能差异:
-
优化后的动画:使用
requestAnimationFrame
和transform
,性能最佳。 -
未优化的
requestAnimationFrame
动画:使用left
,性能稍差。 -
未优化的
setTimeout
动画:使用left
,性能最差。
13.总结
通过优化 DOM 操作、启用硬件加速、减少重排和重绘,可以显著提升动画性能。同时,注意兼容性和资源管理,确保动画在不同设备和浏览器中都能流畅运行。