requestAnimationFrame 用法、優勢與應用場景
瀏覽器會在下次重繪(repaint)前呼叫你傳入的 callback,頻率通常與螢幕刷新率對齊——這就是
requestAnimationFrame做動畫與視覺更新時比setTimeout/setInterval更合適的原因。
一、是什麼?
requestAnimationFrame(簡稱 rAF)是瀏覽器提供的 API,用來在下一幀繪製前執行回呼。
依 MDN 的說明,它適合驅動視覺動畫:瀏覽器會盡量把執行時機對齊螢幕刷新(例如 60Hz ≈ 每秒 60 次),並在頁面不可見時自動降低或暫停,減少不必要的運算。
很多人只把它用在「純動畫」,但其實只要牽涉到跟畫面同步的更新(捲動、layout 變化、遊戲迴圈等),都可以用來做效能優化。
二、基本用法
1. 單次呼叫
回傳值是一個數字 ID,之後可用來取消:
function tick() {
console.log('您好,我是 requestAnimationFrame');
}
const rafId = requestAnimationFrame(tick);
單次呼叫只會執行一次;動畫需要持續更新畫面時,必須在 callback 裡再排下一幀。
2. 遞迴呼叫(常見寫法)
function tick() {
// 更新 DOM / canvas / 樣式…
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
呼叫頻率通常與顯示器刷新率匹配(例如 75Hz 就約每秒 75 次)。用定時器很難穩定適配各種螢幕幀率。
3. 回呼參數:時間戳
callback 只會收到一個參數:DOMHighResTimeStamp(毫秒),代表該幀開始時相對於 performance.now() 時間原點的時間。
function tick(timestamp) {
console.log('requestAnimationFrame 回呼:', timestamp);
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
同一幀內若排了多個 rAF,它們會收到相同的時間戳(都對齊同一幀)。
4. 終止執行:cancelAnimationFrame
語法類似 clearTimeout:把 requestAnimationFrame 的回傳 ID 傳進去即可取消尚未執行的那一幀。
<template>
<button type="button" @click="stop">停止</button>
</template>
<script setup lang="ts">
let myReq = 0;
function tick(timestamp: number) {
console.log('requestAnimationFrame 回呼:', timestamp);
myReq = requestAnimationFrame(tick);
}
myReq = requestAnimationFrame(tick);
function stop() {
cancelAnimationFrame(myReq);
}
</script>
在 Vue 元件裡,記得在 onUnmounted 取消,避免離開頁面後仍持續排程。
三、相對定時器的優勢
1. 動畫更穩、較不易卡頓
瀏覽器會配合顯示器的 VSync 安排繪製週期,rAF 與重繪同頻,幀間隔較穩定(60Hz 約每 16.7ms 一幀)。
setTimeout / setInterval 走任務佇列,會受宏任務、微任務與其他腳本影響,無法保證固定間隔,也無法自動對齊螢幕刷新率,動畫容易抖或掉幀。
| 面向 | requestAnimationFrame | setTimeout / setInterval |
|---|---|---|
| 與螢幕刷新對齊 | 是 | 否 |
| 間隔穩定性 | 相對穩定 | 易受佇列影響 |
| 頁面在背景 | 通常暫停/降頻 | 多數仍會跑(節流程度因瀏覽器而異) |
| 典型用途 | 視覺動畫、跟畫面同步的更新 | 非視覺的延遲/輪詢 |
2. 切到背景會暫停,較省資源
頁面切到背景(或不可見)時,rAF 通常會暫停;切回前景後再繼續。這能減少背景分頁的無謂計算,對筆電電量與 CPU 較友善。
四、應用場景
1. 常規動畫(旋轉範例)
掛載時開始 rAF 迴圈,依 timestamp 算角度;卸載時 cancelAnimationFrame。
<template>
<div class="container">
<div :style="imgStyle" class="earth"></div>
</div>
</template>
<script setup>
import { reactive, onMounted, onUnmounted } from 'vue';
const imgStyle = reactive({
transform: 'rotate(0deg)',
});
let rafId = null;
function animate(time) {
// 用時間戳控制轉速,避免每幀硬加固定角度造成速度不穩
const angle = (time % 10000) / 5;
imgStyle.transform = `rotate(${angle}deg)`;
rafId = window.requestAnimationFrame(animate);
}
onMounted(() => {
rafId = window.requestAnimationFrame(animate);
});
onUnmounted(() => {
if (rafId != null) {
window.cancelAnimationFrame(rafId);
}
});
</script>
<style scoped>
.container {
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
}
.earth {
width: 100px;
height: 100px;
border-radius: 50%;
background-size: cover;
/* 換成實際圖片路徑 */
background-image: url('/path/to/image.png');
}
</style>
2. 滾動載入(scroll + rAF 節流)
scroll 事件觸發極為頻繁。實務上可先取消上一幀未執行的 rAF,再排下一幀檢查是否接近底部——讓「是否該載入」的判斷落在繪製節奏上,減少多餘計算。
<template>
<div ref="scrollRef" class="container">
<div v-for="(item, index) in items" :key="index" class="item">
{{ item }}
</div>
<div v-if="loading" class="loading">資料載入中…</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
const loading = ref(false);
let rafId: number | null = null;
const items = ref<string[]>(
Array.from({ length: 50 }, (_, i) => `Test ${i + 1}`),
);
const scrollRef = ref<HTMLElement | null>(null);
const moreData = () =>
new Promise<void>((resolve) => {
setTimeout(() => {
const start = items.value.length;
items.value.push(
...Array.from({ length: 50 }, (_, i) => `Test ${start + i + 1}`),
);
resolve();
}, 1000);
});
const checkScrollPosition = () => {
if (loading.value) return;
const container = scrollRef.value;
if (!container) return;
const { scrollTop, clientHeight, scrollHeight } = container;
if (scrollHeight - scrollTop - clientHeight <= 100) {
void startLoading();
}
};
const startLoading = async () => {
loading.value = true;
await moreData();
loading.value = false;
};
const handleScroll = () => {
if (rafId !== null) {
window.cancelAnimationFrame(rafId);
}
rafId = window.requestAnimationFrame(checkScrollPosition);
};
onMounted(() => {
scrollRef.value?.addEventListener('scroll', handleScroll);
});
onUnmounted(() => {
if (rafId !== null) {
window.cancelAnimationFrame(rafId);
}
scrollRef.value?.removeEventListener('scroll', handleScroll);
});
</script>
<style scoped>
.container {
max-width: 800px;
height: 600px;
margin: 0 auto;
padding: 20px;
overflow-y: auto;
}
.item {
padding: 10px;
border-bottom: 1px solid #ccc;
}
.loading {
padding: 10px;
color: #999;
text-align: center;
}
</style>
為什麼這樣做?
- 效能:不在每次
scroll立刻做重邏輯,而是合併到下一幀。 - 體感:判斷與繪製節奏接近,滾動過程較不易因頻繁 JS 而抖。
五、還能用在哪裡?
只要畫面在「動」或要跟幀同步,都可考慮 rAF,例如:
- 遊戲主迴圈、粒子/Canvas 繪製
- 進度條、數字滾動、頁面過渡動畫
- 動態變更 layout 後再量測/修正位置(避免強制同步 layout 的抖動)
六、小結
- 用法:通常在 callback 內再呼叫自己形成迴圈;用回傳 ID +
cancelAnimationFrame停止。 - 優勢:對齊刷新率、幀較穩;背景頁面多半會暫停。
- 場景:不限 CSS/Canvas 動畫;滾動節流、遊戲、動態 layout 都能用。