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; // 每次移動 2pxrafBox.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; // 每次移動 2pxtimeoutBox.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,則使用 clearTimeoutwindow.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,則使用 clearTimeoutwindow.clearTimeout(id);});})();// requestAnimationFrame 動畫const rafBox = document.getElementById('rafBox');let rafPosition = 0;let rafAnimationId;function rafAnimate() {rafPosition += 2; // 每次移動 2pxrafBox.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; // 每次移動 2pxtimeoutBox.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; // 每次移動 2pxoptimizedBox.style.transform = `translateX(${optimizedPosition}px)`; // 使用 transformif (optimizedPosition < 600) { // 移動到 600px 時停止optimizedAnimationId = requestAnimFrame(optimizedAnimate);}}// 未優化的 requestAnimationFrame 動畫(使用 left)const unoptimizedRAFBox = document.getElementById('unoptimizedRAFBox');let unoptimizedRAFPosition = 0;let unoptimizedRAFAnimationId;function unoptimizedRAFAnimate() {unoptimizedRAFPosition += 2; // 每次移動 2pxunoptimizedRAFBox.style.left = unoptimizedRAFPosition + 'px'; // 使用 leftif (unoptimizedRAFPosition < 600) { // 移動到 600px 時停止unoptimizedRAFAnimationId = requestAnimFrame(unoptimizedRAFAnimate);}}// 未優化的 setTimeout 動畫(使用 left)const unoptimizedTimeoutBox = document.getElementById('unoptimizedTimeoutBox');let unoptimizedTimeoutPosition = 0;let unoptimizedTimeoutAnimationId;function unoptimizedTimeoutAnimate() {unoptimizedTimeoutPosition += 2; // 每次移動 2pxunoptimizedTimeoutBox.style.left = unoptimizedTimeoutPosition + 'px'; // 使用 leftif (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 操作、啟用硬件加速、減少重排和重繪,可以顯著提升動畫性能。同時,注意兼容性和資源管理,確保動畫在不同設備和瀏覽器中都能流暢運行。
?