requestAnimationFrame (sering disingkat rAF) adalah cara JavaScript bilang ke browser: "sebelum kamu menggambar frame berikutnya, jalankan fungsi ini dulu". Bayangin angkot yang berangkat dari terminal setiap 16,7 ms. Kamu titip pesan ke sopirnya: "sebelum berangkat, kerjakan ini". Sopir mengerjakannya sekali, sambil mencatat jam berangkatnya. Mau dikerjakan lagi di keberangkatan berikutnya? Titip lagi. Terminalnya tutup (tab-nya gak terlihat)? Gak ada keberangkatan, titipanmu menunggu.
🧪 Tebakan cepat: Total harga di kartu Bu Sri menghitung naik dari Rp 700.000 ke Rp 1.050.000. Kodenya menambah 350.000 / 18 = 19.444 di setiap frame, karena "300 ms = 18 frame". Di laptop yang layarnya 120 Hz, berapa lama hitungan itu selesai? Jawabannya ada di bagian "Hitung dari waktu, bukan dari jumlah frame".
Satu titipan, satu frame
function step(timestamp) {
console.log('frame di', timestamp);
requestAnimationFrame(step); // titip lagi untuk frame berikutnya
}
const id = requestAnimationFrame(step); // titipan pertama
requestAnimationFrame(step)menjadwalkanstepsekali saja, tepat sebelum browser menggambar frame berikutnya. Nilai kembaliannya angka id (misalnya145) untuk membatalkan titipan itu.- Browser memanggil
stepdengan satu argumen: timestamp, waktu frame itu dalam milidetik, dari jam yang sama denganperformance.now(). TipenyaDOMHighResTimeStamp, jadi ada pecahannya (10234.5). Semua callback di frame yang sama menerima timestamp yang sama persis; kami cek di Chromium 147, WebKit 26.4, dan Firefox 146. - Supaya jadi loop,
stepmemanggilrequestAnimationFrame(step)lagi di dalam dirinya. Tanpa baris itu, animasinya berhenti setelah satu frame.
Berapa kali per detik? Sebanyak refresh rate layarnya. Kode yang sama di satu laptop kami: Chromium dan Firefox memanggil step 120 kali per detik (jarak 8,3 ms), WebKit 60 kali (jarak 16,7 ms).
Bedanya dengan setInterval(fn, 16): interval jalan menurut jam, bukan menurut frame. Dia bisa jalan dua kali dalam satu frame atau terlewat di frame lain, dan tetap jalan di tab yang tersembunyi. rAF selalu pas satu kali per frame yang digambar.
Hitung dari waktu, bukan dari jumlah frame
Kode di tebakan menganggap satu frame = 16,7 ms. Kami jalankan hitungan Rp 700.000 ke Rp 1.050.000 dengan dua cara di satu laptop:
| Tambah 19.444 per frame | Hitung dari waktu (300 ms) | |
|---|---|---|
| WebKit 26.4, 60 Hz | 18 frame, selesai 303 ms | 20 frame, selesai 314 ms |
| Chromium 147, 120 Hz | 18 frame, selesai 150 ms | 36 frame, selesai 298 ms |
| Tiap frame berat 25 ms | 18 frame, selesai 454 ms | 13 frame, selesai 336 ms |
Itu jawaban tebakannya: 150 ms, dua kali lebih cepat, karena 18 frame di 120 Hz cuma 18 × 8,3 ms. Kalau tiap frame berat (HP lambat, halaman sibuk), kebalikannya: molor jadi 454 ms. Versi "dari waktu" selalu sekitar 300 ms; yang berubah cuma jumlah frame-nya.
Caranya: ubah waktu yang sudah lewat jadi progress t dari 0 sampai 1, lalu ambil nilai di antara awal dan tujuan.
function createAnimation(from, to, duration) {
return {
getValueAt(elapsedMs) {
const t = Math.min(Math.max(elapsedMs / duration, 0), 1); // tahan di 0..1
return from + (to - from) * t;
},
};
}
Contoh createAnimation(0, 300, 1000):
elapsedMs |
t |
Nilai |
|---|---|---|
| 0 | 0 | 0 |
| 500 | 0,5 | 150 |
| 1000 | 1 | 300 |
| 1500 | 1 (ditahan) | 300 |
| -50 | 0 (ditahan) | 0 |
Math.min(..., 1)menahan nilai di tujuan. Tanpa itu, di 1.500 ms nilainya jadi 450, melewati tujuan.Math.max(..., 0)menahan waktu negatif di awal. Ini bukan teori: timestamp frame pertama kadang lebih kecil dariperformance.now()yang kamu catat sebelum memanggil rAF (2 dari 20 percobaan di Chromium, 1 dari 20 di Firefox). Cara paling aman: jadikan timestamp frame pertama sebagai jam mulai.- Rentang yang gak mulai dari nol tetap sama:
createAnimation(100, 200, 400).getValueAt(100)= 100 + 100 × 0,25 = 125.
Untuk gerak yang terus jalan (fisika, game), pakai selisih waktu antar frame, delta time: dt = timestamp - timestampSebelumnya, lalu x += kecepatan * dt / 1000 dengan kecepatan dalam px per detik.
Contoh: total harga menghitung naik
Easing di JavaScript cuma fungsi yang mengubah t jadi t baru. easeOutQuad(t) = t * (2 - t) mulai cepat lalu melambat, mirip ease-out:
const total = document.querySelector('.total b');
let frameId = 0;
function easeOutQuad(t) {
return t * (2 - t); // sama dengan 1 - (1 - t) * (1 - t)
}
function tweenTotal(from, to, duration = 300) {
cancelAnimationFrame(frameId); // hentikan hitungan yang masih jalan
let start = null;
function step(now) {
if (start === null) start = now; // jam mulai = frame pertama
const t = Math.min((now - start) / duration, 1);
const value = from + (to - from) * easeOutQuad(t);
total.textContent = 'Rp ' + Math.round(value).toLocaleString('id-ID');
if (t < 1) frameId = requestAnimationFrame(step);
}
frameId = requestAnimationFrame(step);
}
tweenTotal(700000, 1050000); // dari 2 malam ke 3 malam
| Waktu | t |
easeOutQuad(t) |
Yang tampil |
|---|---|---|---|
| 0 ms | 0 | 0 | Rp 700.000 |
| 75 ms | 0,25 | 0,4375 | Rp 853.125 |
| 150 ms | 0,5 | 0,75 | Rp 962.500 |
| 225 ms | 0,75 | 0,9375 | Rp 1.028.125 |
| 300 ms | 1 | 1 | Rp 1.050.000 |
- Di tengah waktu (
t= 0,5), angkanya sudah 75% jalan. Itu ciri ease-out: sisa jaraknya ditempuh pelan. toLocaleString('id-ID')menulis titik pemisah ribuan:1.050.000.- Untuk yang menyalakan "Kurangi gerakan" (
prefers-reduced-motion: reduce), lewati loop-nya: tulis angka akhir langsung, cukup dengan fade 150 ms.
Menghentikan loop: cancelAnimationFrame
cancelAnimationFrame(id) membatalkan titipan yang belum jalan; callback-nya gak dipanggil sama sekali. Tiap requestAnimationFrame memberi id baru, jadi simpan yang terakhir (frameId = requestAnimationFrame(step) di dalam loop).
Kenapa penting? Kalau tamu mengklik "3 malam" lalu "1 malam" dalam 300 ms dan kamu gak membatalkan, dua loop jalan bersamaan: tiap frame dua callback menulis angka, kerjanya dobel. Di React, batalkan di cleanup effect supaya loop gak terus jalan setelah komponennya hilang.
Saat tab atau iframe gak terlihat
Kalau tab-nya di belakang (document.visibilityState jadi 'hidden'), browser berhenti memanggil rAF; MDN menyebut ini perilaku sebagian besar browser, demi hemat baterai. Begitu tab kembali, timestamp berikutnya melompat jauh. Hitungan berbasis waktu langsung selesai di angka akhir (t ditahan di 1). Loop fisika dengan dt perlu batas, misalnya dt = Math.min(dt, 100), supaya benda gak "teleport".
Iframe juga diperlambat, tapi beda tiap browser. Hasil ukur kami, callback per detik:
| Iframe | Chromium 147 | WebKit 26.4 | Firefox 146 |
|---|---|---|---|
| Beda origin, di luar layar | 0 | 0 | 1 |
Satu origin, display: none |
120 | 61 | 0 |
Jadi jangan menghitung waktu dari jumlah callback. Hitung dari timestamp.
rAF atau CSS / Web Animations API?
rAF tepat untuk nilai yang kamu hitung sendiri tiap frame: menggambar di <canvas>, fisika (kecepatan, pantulan, gesekan), dan nilai yang gak bisa diungkapkan CSS, seperti angka di dalam teks atau posisi scroll.
Untuk gerak UI biasa (transform, opacity), CSS atau Web Animations API lebih baik. Dari pelajaran pertama: saat main thread sibuk 500 ms di tengah animasi, CSS transition tetap menggambar 61 frame, Web Animations API 60 frame, rAF cuma 1. rAF selalu jalan di main thread, jadi ikut membeku.
⚠️ Jebakan yang sering ketemu
- Lupa memanggil
requestAnimationFramelagi di dalam callback. Animasinya cuma satu frame. - Menambah nilai per frame.
x += 2jalan dua kali lebih cepat di layar 120 Hz dan molor di HP lambat. Hitung dari timestamp. - Gak menahan
tdi 0..1. Di 1.500 ms nilainya lewat tujuan (450, bukan 300), dan timestamp frame pertama bisa lebih kecil dari jam mulaimu. - Gak menyimpan id dan membatalkannya. Loop menumpuk setiap klik dan tetap jalan setelah elemennya hilang.
- Kerja berat di dalam callback. Budget-nya cuma 8,3 ms di 120 Hz; lebih dari itu, frame terlewat.
TL;DR: requestAnimationFrame(cb) memanggil cb sekali, tepat sebelum frame berikutnya, dengan timestamp frame itu (ms, jam yang sama dengan performance.now()); untuk loop, panggil lagi di dalam cb. Hitung progress dari waktu: t = Math.min(Math.max(elapsed / duration, 0), 1), nilai = from + (to - from) * t, dan easing seperti easeOutQuad(t) = t * (2 - t) (0,5 jadi 0,75). Menambah per frame selesai dalam 150 ms di 120 Hz, bukan 300 ms. Simpan id dan cancelAnimationFrame(id) sebelum mulai lagi atau saat elemen hilang. Tab tersembunyi menghentikan rAF; iframe diperlambat beda-beda tiap browser. Pakai rAF untuk canvas, fisika, dan nilai yang gak bisa ditulis di CSS (angka total Rp 700.000 ke Rp 1.050.000); untuk transform dan opacity, CSS atau Web Animations API tetap jalan walau main thread sibuk.