Performance & Best Practices — Web Animation

Animation Performance & Best Practices Animasi yang lambat atau jank lebih buruk daripada tidak ada animasi sama sekali. Memahami bagaimana browser merender…

Animasi yang mulus artinya setiap frame selesai dikerjakan sebelum layar butuh frame berikutnya. Ingat angkot di pelajaran requestAnimationFrame: dia berangkat setiap 16,7 ms di layar 60 Hz, atau setiap 8,3 ms di layar 120 Hz. Kalau pekerjaanmu belum beres saat angkot berangkat, angkot gak menunggu; frame itu terlewat, dan matamu melihat animasi tersendat (jank). Pelajaran ini tentang cara mengukur di mana waktunya habis, bukan menebak.

🧪 Tebakan cepat: Halaman daftar kamar punya 300 kartu. Kode ini membuat foto tiap kartu berproporsi 4:3: for (const f of foto) f.style.height = f.offsetWidth * 0.75 + 'px';. Berapa kali browser menghitung layout di dalam loop itu? Jawabannya ada di bagian "Layout thrashing dan forced reflow".

Anggaran satu frame

1.000 ms dibagi 60 frame = 16,7 ms; dibagi 120 frame = 8,3 ms. Di laptop yang sama, kami mengukur Chromium dan Firefox memanggil rAF 120 kali per detik, WebKit 60 kali. Dalam anggaran itu browser mengerjakan urutan yang sudah kamu kenal: JavaScript, Style, Layout, Paint, Composite. Semuanya kecuali composite terjadi di main thread, thread yang sama yang menjalankan kode dan menanggapi klik.

Frame yang makan 25 ms di layar 120 Hz melewatkan dua keberangkatan. Di HP murah, pekerjaan yang sama bisa empat sampai enam kali lebih lambat.

Ukur dulu: panel Performance

Di Chrome DevTools, tab Performance:

  1. Di pengaturan panel, pilih CPU: 4x slowdown supaya laptopmu berlagak seperti HP.
  2. Klik record, mainkan animasinya, stop.
  3. Baris Frames menandai frame yang terlewat atau cuma tergambar sebagian.
  4. Baris Main menunjukkan pekerjaannya: kuning untuk JavaScript, ungu untuk Style dan Layout, hijau untuk Paint. Tugas di atas 50 ms diberi sudut merah (long task), dan layout yang dipaksa JavaScript diberi peringatan "Forced reflow".

Di tab Rendering (Ctrl+Shift+P, ketik "rendering"): Paint flashing mewarnai hijau area yang di-paint ulang, Layer borders menggaris tiap layer, Frame Rendering Stats menampilkan FPS. Firefox punya Firefox Profiler, Safari punya tab Timelines.

Long Animation Frames API

Dari kode, kamu bisa diberi tahu tiap frame yang lebih dari 50 ms beserta penyebabnya:

new PerformanceObserver((list) => {
  for (const frame of list.getEntries()) {
    const paksa = frame.scripts.reduce((ms, s) => ms + s.forcedStyleAndLayoutDuration, 0);
    console.log(`frame ${Math.round(frame.duration)} ms, layout dipaksa ${Math.round(paksa)} ms`);
  }
}).observe({ type: 'long-animation-frame', buffered: true });

Untuk loop di tebakan, Chromium 147 melaporkan satu frame 51 ms dengan layout dipaksa 47 sampai 49 ms. Dukungannya: Chrome 123 ke atas; Firefox dan Safari belum (data MDN, Oktober 2026). Di browser lain, ukur jarak antar frame sendiri dengan requestAnimationFrame.

Layout thrashing dan forced reflow

Menulis style membuat layout kotor. Membaca ukuran (offsetWidth, offsetHeight, offsetTop, getBoundingClientRect(), getComputedStyle(), scrollTop) memaksa browser menghitung layout saat itu juga supaya jawabannya benar: itu forced reflow. Loop yang menulis lalu membaca berselang-seling memaksanya di setiap putaran: layout thrashing.

// Selang-seling: tiap putaran menulis, lalu putaran berikutnya membaca
for (const f of foto) f.style.height = f.offsetWidth * 0.75 + 'px';

// Dikumpulkan: baca semua dulu, baru tulis semua
const lebar = foto.map((f) => f.offsetWidth);
foto.forEach((f, i) => (f.style.height = lebar[i] * 0.75 + 'px'));

Kami jalankan keduanya pada 300 kartu kamar:

Selang-seling Dikumpulkan
Chromium 147 300 layout, 49 sampai 50 ms 1 layout, 0,3 sampai 0,4 ms
WebKit 26.4 69 sampai 73 ms di bawah 1 ms
Firefox 146 128 sampai 134 ms 1 ms

Itu jawaban tebakannya: 300 kali, satu per kartu. Di Chromium 120 Hz, 50 ms itu berarti 6 frame terlewat. Versi yang dikumpulkan cuma sekali. Lebih baik lagi tanpa JavaScript: .photo { aspect-ratio: 4 / 3; }.

Properti murah dan mahal

Dari pengukuran di pelajaran 5 (kartu naik 16 px dalam 300 ms, Chromium 147): lewat top, layout 35 sampai 37 kali; lewat transform, 1 kali. box-shadow yang menebal: 74 kali paint. transform dan opacity adalah properti yang cukup sampai composite: layer yang sudah di-paint cuma disusun ulang, dan pekerjaan itu bisa jalan di compositor thread. Animasikan dua properti itu, hindari width, height, top, left, margin, dan padding.

will-change: satu layer, satu harga

will-change: transform menyiapkan layer sebelum animasi mulai. Tiap layer adalah bitmap di memori GPU: kartu 328 × 143 px di layar 3x sekitar 1,7 MB, dan 30 kartu menambah 30 layer, sekitar 51 MB. Memasangnya di semua elemen sebagai "optimasi" membuat terlalu banyak layer: memori GPU habis dan compositing justru melambat. Pasang di elemen yang memang akan bergerak, sebentar, lalu lepas.

Terlalu banyak animasi sekaligus

Skeleton loading dengan kilau (shimmer) sering dipasang di setiap kartu. Kami ukur 300 kartu skeleton selama 2 detik:

Kilau Chromium 147 Chromium, CPU 4x lebih lambat Firefox 146 WebKit 26.4 (60 Hz)
background-position di tiap kartu 241 frame; style 145 ms + paint 399 ms 83 frame, frame terlama 617 ms 78 sampai 84 frame 122 frame
transform di ::after tiap kartu 241 frame; style 239 ms, paint 0 compositor 240 frame, tapi rAF cuma 76 157 sampai 217 frame 122 frame
satu ::after untuk seluruh daftar 241 frame; style 18 ms, paint 0 241 frame 87 sampai 102 frame 121 frame

Yang terlihat: background-position di-paint ulang di setiap frame; 300 animasi berarti 300 pembaruan style per frame walau cuma transform; dan browser berbeda menang di cara yang berbeda (Firefox lebih suka kilau per kartu daripada satu layer raksasa). Makanya diukur. Kurangi jumlahnya: kilau hanya untuk kartu yang terlihat, hentikan begitu data datang, dan matikan untuk "Kurangi gerakan".

content-visibility: auto melewati render kartu yang di luar layar (Chrome 85, Firefox 125, Safari 26): style 300 kartu background-position turun dari 145 ke 52 ms. contain: layout paint (Chrome 52, Firefox 69, Safari 15.4) membatasi perubahan di dalam kartu supaya gak menyebar ke seluruh halaman.

⚠️ Jebakan yang sering ketemu

TL;DR: Anggaran satu frame 16,7 ms (60 Hz) atau 8,3 ms (120 Hz), dan main thread mengerjakan JavaScript, style, layout, dan paint di dalamnya. Ukur dengan panel Performance (CPU 4x slowdown, baris Frames dan Main, peringatan Forced reflow) dan, di Chrome 123 ke atas, Long Animation Frames API (duration, blockingDuration, scripts[].forcedStyleAndLayoutDuration). Membaca offsetWidth setelah menulis style memaksa layout: loop selang-seling di 300 kartu = 300 layout (Chromium 49 sampai 50 ms, WebKit sekitar 70, Firefox sekitar 130) lawan 1 layout dan di bawah 1 ms kalau dibaca dulu semuanya. Animasikan transform dan opacity (compositor), bukan top (35 sampai 37 layout) atau box-shadow. will-change di semua elemen = terlalu banyak layer (kartu di layar 3x sekitar 1,7 MB). 300 animasi sekaligus membebani main thread walau cuma transform; kurangi jumlahnya, pakai content-visibility: auto, dan ukur di browser yang dipakai pengunjungmu.

Yang akan kamu pelajari