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:
- Di pengaturan panel, pilih CPU: 4x slowdown supaya laptopmu berlagak seperti HP.
- Klik record, mainkan animasinya, stop.
- Baris Frames menandai frame yang terlewat atau cuma tergambar sebagian.
- 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 });
duration: panjang frame itu.blockingDuration: bagian yang melewati 50 ms, saat klik gak bisa ditanggapi.scripts: script yang jalan di frame itu, masing-masing dengansourceURL,invoker(misalnya"BUTTON#pesan.onclick"), danforcedStyleAndLayoutDuration.
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
- Menebak, bukan mengukur. Rekam dulu di panel Performance dengan CPU diperlambat; angka di laptop cepat menipu.
- Membaca ukuran di dalam loop yang menulis style. Kumpulkan semua baca, lalu semua tulis, atau pakai CSS (
aspect-ratio). will-changedi semua elemen. Puluhan MB memori GPU untuk layer yang gak pernah bergerak.- Menganimasikan
box-shadow,width, atautop. Paint atau layout di setiap frame. Pakaitransformdanopacity. - Mengandalkan LoAF di semua browser. Cuma Chromium; di Firefox dan Safari, ukur jarak antar frame dengan requestAnimationFrame.
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.