requestAnimationFrame (sering disingkat rAF) adalah cara bilang ke browser: "panggil function ini tepat sebelum kamu menggambar frame berikutnya." Layar biasa menggambar ulang 60 kali per detik, jadi satu frame sekitar 16,7 ms. Layar 120 Hz menggambar 120 kali per detik. Bayangin flipbook, buku gambar yang dibalik cepat: gerakannya muncul karena tiap halaman digambar sedikit beda, lalu dibalik dengan irama tetap. rAF memastikan kamu menggambar halaman berikutnya tepat sebelum halaman itu dibalik, bukan di sembarang waktu.
🧪 Tebakan cepat: animasi menggeser kotak 4 px tiap frame. Di laptop 60 Hz, kotaknya sampai dalam 1 detik. Di HP dengan layar 120 Hz, berapa lama: 1 detik, setengah detik, atau 2 detik? Jawabannya ada di bagian "Gerak dari waktu, bukan dari jumlah frame".
Cara kerjanya
const box = document.querySelector(".box");
let x = 0;
function frame() {
x += 4; // 4 px tiap frame
box.style.transform = `translateX(${x}px)`;
if (x < 240) requestAnimationFrame(frame); // minta frame berikutnya
}
requestAnimationFrame(frame);
requestAnimationFrame(frame)menjadwalkanframesekali, sebelum frame berikutnya digambar. Supaya jadi animasi,framemenjadwalkan dirinya lagi.- Browser memberi callback satu argumen, timestamp: waktu frame itu dalam milidetik, dengan skala yang sama dengan
performance.now(). - Callback-nya jalan sekitar 60 kali per detik di layar 60 Hz, dan lebih sering di layar 120 Hz. Kecepatannya mengikuti layar.
- Di tab yang gak kelihatan, rAF berhenti dipanggil sampai tab-nya dibuka lagi. Hemat baterai.
- rAF jalan di main thread. Kalau main thread lagi sibuk, frame bisa terlewat. Jadi rAF gak menjamin tiap frame sama panjang.
Kenapa bukan setInterval
setInterval(fn, 16) jalan dengan jamnya sendiri, gak peduli kapan layar menggambar. Ada frame yang kebagian dua kali update, ada yang gak kebagian sama sekali, dan animasinya terasa patah-patah.
setInterval(fn, 16) |
requestAnimationFrame(fn) |
|
|---|---|---|
| Kapan jalan | tiap kira-kira 16 ms, jamnya sendiri | tepat sebelum frame digambar |
| Di layar 120 Hz | tetap sekitar 60 kali per detik | 120 kali per detik |
| Di tab yang tersembunyi | tetap jalan, diperlambat jadi paling sering sekitar sekali per detik | berhenti |
Gerak dari waktu, bukan dari jumlah frame
Kode di bagian "Cara kerjanya" maju 4 px tiap frame. Di layar 60 Hz, 240 px itu 60 frame, jadi 1 detik. Di layar 120 Hz, 60 frame selesai dalam setengah detik. Itu jawaban tebakannya: setengah detik, dua kali lebih cepat. Di HP yang lagi sibuk dan cuma sempat menggambar 30 frame per detik, malah jadi 2 detik.
Supaya kecepatannya sama di layar mana pun, hitung posisi dari waktu yang sudah lewat: progress = elapsedMs / duration, dari 0 sampai 1. Ini isi latihannya:
function calculateAnimationProgress(startTime, currentTime, duration) {
const elapsedMs = currentTime - startTime;
const progress = Math.min(elapsedMs / duration, 1); // gak boleh lebih dari 1
return { progress, isComplete: progress >= 1, elapsedMs };
}
const box = document.querySelector(".box");
let startTime = null;
function frame(timestamp) {
if (startTime === null) startTime = timestamp; // frame pertama
const { progress, isComplete } = calculateAnimationProgress(startTime, timestamp, 1000);
box.style.transform = `translateX(${progress * 240}px)`;
if (!isComplete) requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
| Panggilan | elapsedMs |
progress |
isComplete |
|---|---|---|---|
calculateAnimationProgress(0, 500, 1000) |
500 | 0.5 | false |
calculateAnimationProgress(0, 1000, 1000) |
1000 | 1 | true |
calculateAnimationProgress(0, 1500, 1000) |
1500 | 1 | true |
calculateAnimationProgress(100, 100, 1000) |
0 | 0 | false |
Math.min(..., 1)menahanprogressdi 1, jadi kotaknya berhenti di 240 px, gak kebablasan. Yang ditahan cumaprogress:elapsedMstetap 1500.isCompletebernilaitruebegituprogresssampai 1, dan loop berhenti.- Di 60 Hz kotaknya melewati sekitar 60 frame, di 120 Hz sekitar 120 frame, tapi dua-duanya sampai di 1.000 ms. Bedanya cuma lebih halus.
Untuk gerak tanpa durasi, misalnya bola di game, pakai delta time: selisih timestamp frame ini dengan frame sebelumnya. x += 240 * (delta / 1000) artinya 240 px per detik, berapa pun jumlah frame-nya.
Menghentikan animasi
let id = requestAnimationFrame(frame); // tiap frame, simpan id yang terbaru
// di dalam frame: id = requestAnimationFrame(frame);
cancelAnimationFrame(id); // frame yang sudah dijadwalkan batal
requestAnimationFrame mengembalikan angka id. cancelAnimationFrame(id) membatalkan callback yang belum jalan, jadi loop-nya berhenti. Wajib dipanggil waktu elemennya dibuang dari halaman, misalnya saat komponen di-unmount. Kalau gak, loop-nya terus jalan menggerakkan elemen yang sudah gak ada.
⚠️ Jebakan yang sering ketemu
- Gerak tetap per frame.
x += 4dua kali lebih cepat di layar 120 Hz. Hitung dari waktu. - Lupa menahan
progress. TanpaMath.min, progress 1.5 bikin kotaknya lewat ke 360 px. - Ikut menahan
elapsedMs. Di latihan,calculateAnimationProgress(0, 1500, 1000)harus tetap memberielapsedMs: 1500. - Menyimpan id yang pertama saja. Tiap frame menghasilkan id baru, jadi
cancelAnimationFramedengan id lama gak menghentikan apa-apa. - Kerja berat di dalam callback. Lebih dari sekitar 16 ms per frame, dan frame berikutnya terlewat.
TL;DR: requestAnimationFrame(fn) memanggil fn tepat sebelum frame berikutnya digambar, mengikuti layar: sekitar 60 kali per detik di 60 Hz, lebih di 120 Hz, dan berhenti di tab yang tersembunyi. setInterval gak sinkron dengan layar. Hitung gerak dari timestamp: progress = Math.min((currentTime - startTime) / duration, 1), isComplete = progress >= 1, dan elapsedMs dibiarkan apa adanya. Gerak tetap per frame jadi lebih cepat di layar 120 Hz. Simpan id terbaru, lalu cancelAnimationFrame(id) untuk berhenti, termasuk saat elemennya dibuang.