Pengantar
Anda ingin aplikasi web Anda terasa responsif dan lancar saat melakukan animasi, transisi, dan efek UI kecil lainnya. Memastikan efek ini bebas jank dapat berarti perbedaan antara nuansa "native" atau nuansa yang canggung dan tidak rapi.
Ini adalah artikel pertama dari serangkaian artikel yang membahas pengoptimalan performa rendering di browser. Untuk memulai, kita akan membahas alasan animasi yang lancar sulit dibuat dan hal yang perlu dilakukan untuk mencapainya, serta beberapa praktik terbaik yang mudah. Banyak ide ini awalnya dipresentasikan dalam "Jank Busters", sebuah presentasi yang saya dan Nat Duca berikan di presentasi Google I/O (video) tahun ini.
Memperkenalkan V-sync
Gamer PC mungkin sudah tidak asing dengan istilah ini, tetapi jarang digunakan di web: apa yang dimaksud dengan v-sync?
Pertimbangkan layar ponsel Anda: layar dimuat ulang pada interval reguler, biasanya (tetapi tidak selalu) sekitar 60 kali per detik. V-sync (atau sinkronisasi vertikal) mengacu pada praktik pembuatan frame baru hanya di antara refresh layar. Anda mungkin menganggapnya seperti kondisi perlombaan antara proses yang menulis data ke buffer layar dan sistem operasi yang membaca data tersebut untuk menampilkannya di layar. Kita ingin konten frame yang dibuffer berubah di antara pembaruan ini, bukan selama pembaruan; jika tidak, monitor akan menampilkan setengah dari satu frame dan setengah dari frame lainnya, yang menyebabkan "tearing".
Untuk mendapatkan animasi yang lancar, Anda memerlukan frame baru yang siap setiap kali layar dimuat ulang. Hal ini memiliki dua implikasi besar: waktu render frame (yaitu, kapan frame harus siap) dan anggaran frame (yaitu, berapa lama browser harus menghasilkan frame). Anda hanya memiliki waktu antara refresh layar untuk menyelesaikan frame (~16 md pada layar 60 Hz), dan Anda ingin mulai membuat frame berikutnya segera setelah frame terakhir ditampilkan di layar.
Waktu adalah Segalanya: requestAnimationFrame
Banyak developer web menggunakan setInterval
atau setTimeout
setiap 16 milidetik untuk membuat animasi. Hal ini menjadi masalah karena berbagai alasan (dan kita akan membahasnya lebih lanjut dalam beberapa saat), tetapi yang perlu diperhatikan adalah:
- Resolusi timer dari JavaScript hanya berkisar beberapa milidetik
- Perangkat yang berbeda memiliki kecepatan refresh yang berbeda
Ingat masalah pengaturan waktu frame yang disebutkan di atas: Anda memerlukan frame animasi yang telah selesai, yang telah selesai dengan JavaScript, manipulasi DOM, tata letak, gambar, dll., agar siap sebelum pembaruan layar berikutnya terjadi. Resolusi timer yang rendah dapat menyulitkan penyelesaian frame animasi sebelum layar dimuat ulang, tetapi variasi kecepatan refresh layar membuat hal ini tidak mungkin dilakukan dengan timer tetap. Apa pun interval timernya, Anda akan perlahan-lahan keluar dari periode waktu untuk frame dan akhirnya menghapusnya. Hal ini akan terjadi meskipun timer diaktifkan dengan akurasi milidetik, yang tidak akan terjadi (seperti yang telah ditemukan oleh developer) -- resolusi timer bervariasi bergantung pada apakah mesin menggunakan baterai atau dicolokkan, dapat terpengaruh oleh tab latar belakang yang menghabiskan resource, dll. Meskipun hal ini jarang terjadi (misalnya, setiap 16 frame karena Anda mengalami keterlambatan milidetik), Anda akan melihat: Anda akan kehilangan beberapa frame per detik. Anda juga akan melakukan pekerjaan untuk menghasilkan frame yang tidak pernah ditampilkan, yang akan membuang daya dan waktu CPU yang dapat Anda gunakan untuk melakukan hal lain di aplikasi.
Layar yang berbeda memiliki kecepatan refresh yang berbeda: 60 Hz adalah yang umum, tetapi beberapa ponsel memiliki kecepatan refresh 59 Hz, beberapa laptop turun menjadi 50 Hz dalam mode daya rendah, beberapa monitor desktop memiliki kecepatan refresh 70 Hz.
Kita cenderung berfokus pada frame per detik (FPS) saat membahas performa rendering, tetapi varian dapat menjadi masalah yang lebih besar. Mata kita akan melihat hambatan kecil dan tidak teratur dalam animasi yang dapat dihasilkan oleh animasi yang waktunya tidak tepat.
Cara mendapatkan frame animasi yang waktunya tepat adalah dengan requestAnimationFrame
. Saat menggunakan API ini, Anda meminta frame animasi ke browser. Callback Anda akan dipanggil saat browser akan segera menghasilkan frame baru. Hal ini terjadi terlepas dari kecepatan refresh.
requestAnimationFrame
juga memiliki properti bagus lainnya:
- Animasi di tab latar belakang dijeda, sehingga menghemat resource sistem dan masa pakai baterai.
- Jika sistem tidak dapat menangani rendering pada kecepatan refresh layar, sistem dapat membatasi animasi dan menghasilkan callback lebih jarang (misalnya, 30 kali per detik pada layar 60 Hz). Meskipun kecepatan frame berkurang setengahnya, hal ini akan membuat animasi tetap konsisten -- dan seperti yang dinyatakan di atas, mata kita jauh lebih sensitif terhadap varian daripada kecepatan frame. 30 Hz yang stabil terlihat lebih baik daripada 60 Hz yang melewatkan beberapa frame per detik.
requestAnimationFrame
telah dibahas di mana-mana, jadi lihat artikel seperti ini dari creative JS untuk mengetahui info selengkapnya, tetapi ini adalah langkah pertama yang penting untuk membuat animasi yang lancar.
Anggaran Frame
Karena kita ingin frame baru siap di setiap refresh layar, hanya ada waktu di antara refresh untuk melakukan semua pekerjaan guna membuat frame baru. Pada layar 60 Hz, artinya kita memiliki waktu sekitar 16 md untuk menjalankan semua JavaScript, melakukan tata letak, menggambar, dan apa pun yang harus dilakukan browser untuk menampilkan frame. Artinya, jika JavaScript di dalam callback requestAnimationFrame
memerlukan waktu lebih dari 16 md untuk dijalankan, Anda tidak akan dapat menghasilkan frame tepat waktu untuk v-sync.
16 md bukanlah waktu yang lama. Untungnya, Developer Tools Chrome dapat membantu Anda melacak apakah Anda menghabiskan anggaran frame selama callback requestAnimationFrame.
Membuka linimasa Dev Tools dan merekam animasi ini saat berjalan dengan cepat menunjukkan bahwa kita jauh melebihi anggaran saat membuat animasi. Di Linimasa, beralihlah ke "Frame" dan lihat:
Callback requestAnimationFrame (rAF) tersebut memerlukan waktu >200 md. Itu adalah urutan magnitudo yang terlalu lama untuk menandai frame setiap 16 md. Membuka salah satu callback rAF yang panjang akan mengungkapkan apa yang terjadi di dalamnya: dalam hal ini, banyak tata letak.
Video Paul membahas penyebab spesifik dari tata letak ulang (membaca scrollTop
) dan cara menghindarinya secara lebih mendetail. Namun, intinya di sini adalah Anda dapat mempelajari callback dan menyelidiki apa yang memerlukan waktu lama.
Perhatikan waktu render frame 16 md. Ruang kosong dalam frame tersebut adalah headroom yang Anda miliki untuk melakukan lebih banyak pekerjaan (atau membiarkan browser melakukan pekerjaan yang perlu dilakukan di latar belakang). Ruang kosong itu adalah Hal yang Baik.
Sumber Jank Lainnya
Penyebab terbesar masalah saat mencoba menjalankan animasi yang didukung JavaScript adalah hal lain dapat mengganggu callback rAF Anda, dan bahkan mencegahnya berjalan sama sekali. Meskipun callback rAF Anda ramping dan berjalan hanya dalam beberapa milidetik, aktivitas lain (seperti memproses XHR yang baru saja masuk, menjalankan pengendali peristiwa input, atau menjalankan update terjadwal pada timer) dapat tiba-tiba masuk dan berjalan selama jangka waktu tertentu tanpa menghasilkan. Di perangkat seluler, terkadang pemrosesan peristiwa ini dapat memerlukan waktu ratusan milidetik, dan selama waktu tersebut animasi Anda akan benar-benar terhenti. Kami menyebutnya kendala animasi jank.
Tidak ada solusi ajaib untuk menghindari situasi ini, tetapi ada beberapa praktik terbaik arsitektur untuk menyiapkan Anda agar berhasil:
- Jangan melakukan banyak pemrosesan di pengendali input. Melakukan banyak JS atau mencoba mengatur ulang seluruh halaman selama, misalnya, pengendali onscroll adalah penyebab yang sangat umum dari jank yang mengerikan.
- Dorong sebanyak mungkin pemrosesan (baca: apa pun yang akan memerlukan waktu lama untuk dijalankan) ke callback rAF atau Web Worker Anda sebanyak mungkin.
- Jika Anda mendorong pekerjaan ke dalam callback rAF, coba bagi pekerjaan menjadi beberapa bagian sehingga Anda hanya memproses sedikit setiap frame atau menundanya hingga setelah animasi penting selesai. Dengan cara ini, Anda dapat terus menjalankan callback rAF singkat dan menganimasikan dengan lancar.
Untuk tutorial bagus yang membahas cara mendorong pemrosesan ke callback requestAnimationFrame, bukan pengendali input, lihat artikel Paul Lewis Animasi yang Lebih Ringan, Lebih Efisien, dan Lebih Cepat dengan requestAnimationFrame.
Animasi CSS
Apa yang lebih baik daripada JS ringan dalam callback peristiwa dan rAF Anda? Tidak ada JS.
Sebelumnya, kami mengatakan bahwa tidak ada solusi praktis untuk menghindari gangguan pada callback rAF, tetapi Anda dapat menggunakan animasi CSS untuk menghindarinya sepenuhnya. Khusus di Chrome untuk Android (dan browser lain sedang mengerjakan fitur serupa), animasi CSS memiliki properti yang sangat diinginkan sehingga browser sering dapat menjalankannya meskipun JavaScript sedang berjalan.
Ada pernyataan implisit di bagian di atas tentang jank: browser hanya dapat melakukan satu hal dalam satu waktu. Hal ini tidak sepenuhnya benar, tetapi merupakan asumsi kerja yang baik untuk dimiliki: pada waktu tertentu, browser dapat menjalankan JS, melakukan tata letak, atau menggambar, tetapi hanya satu per satu. Hal ini dapat diverifikasi di tampilan Linimasa DevTools. Salah satu pengecualian untuk aturan ini adalah animasi CSS di Chrome untuk Android (dan Chrome desktop dalam waktu dekat, meskipun belum).
Jika memungkinkan, penggunaan animasi CSS akan menyederhanakan aplikasi Anda dan memungkinkan animasi berjalan dengan lancar, bahkan saat JavaScript berjalan.
// see http://paulirish.com/2011/requestanimationframe-for-smart-animating/ for info on rAF polyfills
rAF = window.requestAnimationFrame;
var degrees = 0;
function update(timestamp) {
document.querySelector('#foo').style.webkitTransform = "rotate(" + degrees + "deg)";
console.log('updated to degrees ' + degrees);
degrees = degrees + 1;
rAF(update);
}
rAF(update);
Jika Anda mengklik tombol, JavaScript akan berjalan selama 180 md, sehingga menyebabkan jank. Namun, jika kita mendorong animasi tersebut dengan animasi CSS, jank tidak akan terjadi lagi.
(Perlu diingat bahwa pada saat penulisan ini, animasi CSS hanya bebas jank di Chrome untuk Android, bukan Chrome desktop.)
/* tools like Modernizr (http://modernizr.com/) can help with CSS polyfills */
#foo {
+animation-duration: 3s;
+animation-timing-function: linear;
+animation-animation-iteration-count: infinite;
+animation-animation-name: rotate;
}
@+keyframes: rotate; {
from {
+transform: rotate(0deg);
}
to {
+transform: rotate(360deg);
}
}
Untuk informasi selengkapnya tentang penggunaan Animasi CSS, lihat artikel seperti artikel ini di MDN.
Penutup
Singkatnya:
- Saat menganimasikan, menghasilkan frame untuk setiap refresh layar sangatlah penting. Animasi Vsync memberikan dampak positif yang besar pada nuansa aplikasi.
- Cara terbaik untuk mendapatkan animasi vsync di Chrome dan browser modern lainnya adalah menggunakan animasi CSS. Jika Anda memerlukan fleksibilitas yang lebih besar daripada yang disediakan animasi CSS, teknik terbaiknya adalah animasi berbasis requestAnimationFrame.
- Agar animasi rAF tetap sehat dan menyenangkan, pastikan pengendali peristiwa lainnya tidak mengganggu callback rAF Anda yang berjalan, dan pastikan callback rAF singkat (<15 md).
Terakhir, animasi vsync tidak hanya berlaku untuk animasi UI sederhana, tetapi juga berlaku untuk animasi Canvas2D, animasi WebGL, dan bahkan scroll di halaman statis. Dalam artikel berikutnya dalam seri ini, kita akan membahas performa scroll dengan mempertimbangkan konsep-konsep ini.
Selamat membuat animasi.