requestAnimationFrame — Performance

requestAnimationFrame requestAnimationFrame (rAF) adalah cara yang benar untuk menjalankan animasi di JavaScript. Berbeda dari setTimeout atau setInterval…

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);

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

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

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.

Yang akan kamu pelajari