Skip to main content

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 走任務佇列,會受宏任務、微任務與其他腳本影響,無法保證固定間隔,也無法自動對齊螢幕刷新率,動畫容易抖或掉幀。

面向requestAnimationFramesetTimeout / 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 的抖動)

六、小結​

  1. 用法:通常在 callback 內再呼叫自己形成迴圈;用回傳 ID + cancelAnimationFrame 停止。
  2. 優勢:對齊刷新率、幀較穩;背景頁面多半會暫停。
  3. 場景:不限 CSS/Canvas 動畫;滾動節流、遊戲、動態 layout 都能用。

參考資料​