requestAnimationFrame — Web Animation

requestAnimationFrame requestAnimationFrame (disingkat rAF) adalah API browser untuk menjalankan animasi JavaScript yang sinkron dengan refresh rate layar…

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

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

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

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

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.

Yang akan kamu pelajari