Jank busting untuk performa rendering yang lebih baik

Tom Wiltzius
Tom Wiltzius

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 yang kaku dan tidak rapi.

Artikel ini adalah yang pertama dari rangkaian 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 dari ide ini awalnya dipresentasikan dalam "Jank Busters", sebuah presentasi yang diberikan oleh Nat Duca dan saya di Google I/O (video) tahun ini.

Memperkenalkan V-sync

Gamer PC mungkin sudah tidak asing dengan istilah ini, tetapi istilah ini tidak umum di web: apa itu v-sync?

Pertimbangkan layar ponsel Anda: layar diperbarui secara berkala, biasanya (tetapi tidak selalu) sekitar 60 kali per detik. V-sync (atau sinkronisasi vertikal) mengacu pada praktik pembuatan frame baru hanya di antara pemuatan ulang layar. Anda mungkin menganggap hal ini seperti kondisi race antara proses yang menulis data ke dalam buffer layar dan sistem operasi yang membaca data tersebut untuk ditampilkan di layar. Kita ingin konten frame yang di-buffer berubah di antara refresh ini, bukan selama refresh; 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 diperbarui. Hal ini memiliki dua implikasi besar: pengaturan waktu render frame (yaitu, kapan frame harus siap) dan anggaran frame (yaitu, berapa lama browser harus menghasilkan frame). Anda hanya memiliki waktu di antara refresh layar untuk menyelesaikan satu frame (~16 md pada layar 60 Hz), dan Anda ingin mulai menghasilkan frame berikutnya segera setelah frame terakhir ditampilkan di layar.

Pengaturan waktu itu sangat penting: requestAnimationFrame

Banyak developer web menggunakan setInterval atau setTimeout setiap 16 milidetik untuk membuat animasi. Hal ini menjadi masalah karena berbagai alasan (dan kami akan membahasnya lebih lanjut sebentar lagi), tetapi yang menjadi perhatian khusus adalah:

  • Resolusi timer dari JavaScript hanya dalam urutan beberapa milidetik
  • Perangkat yang berbeda memiliki kecepatan refresh yang berbeda

Ingat kembali masalah pengaturan waktu render frame yang disebutkan di atas: Anda memerlukan frame animasi yang sudah selesai, yang diselesaikan dengan JavaScript, manipulasi DOM, tata letak, rendering, dll., agar siap sebelum refresh layar berikutnya terjadi. Resolusi timer yang rendah dapat menyulitkan penyelesaian frame animasi sebelum refresh layar berikutnya, tetapi variasi kecepatan refresh layar membuatnya tidak mungkin dilakukan dengan timer tetap. Berapa pun interval timer-nya, Anda akan perlahan-lahan keluar dari jendela pengaturan waktu untuk sebuah frame dan akhirnya melewatkan satu frame. 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 dipengaruhi oleh tab latar belakang yang menghabiskan resource, dll. Meskipun hal ini jarang terjadi (misalnya, setiap 16 frame karena Anda meleset satu milidetik), Anda akan menyadarinya: Anda akan kehilangan beberapa frame per detik. Anda juga akan melakukan pekerjaan untuk menghasilkan frame yang tidak pernah ditampilkan, yang akan memboroskan daya dan waktu CPU yang dapat Anda gunakan untuk melakukan hal lain dalam aplikasi.

Layar yang berbeda memiliki kecepatan refresh yang berbeda: 60 Hz adalah kecepatan yang umum, tetapi beberapa ponsel memiliki kecepatan 59 Hz, beberapa laptop turun ke 50 Hz dalam mode hemat daya, dan beberapa monitor desktop memiliki kecepatan 70 Hz.

Kita cenderung berfokus pada frame per detik (FPS) saat membahas performa rendering, tetapi varians dapat menjadi masalah yang lebih besar. Mata kita dapat melihat hambatan kecil dan tidak teratur dalam animasi yang dapat dihasilkan oleh animasi dengan pengaturan waktu yang buruk.

Cara untuk mendapatkan frame animasi dengan pengaturan waktu yang benar adalah dengan requestAnimationFrame. Saat menggunakan API ini, Anda meminta frame animasi dari browser. Callback Anda akan dipanggil saat browser akan segera menghasilkan frame baru. Hal ini terjadi terlepas dari kecepatan refresh yang digunakan.

requestAnimationFrame juga memiliki properti bagus lainnya:

  • Animasi di tab latar belakang akan 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 hal ini mengurangi kecepatan frame menjadi setengahnya, animasi tetap konsisten -- dan seperti yang dinyatakan di atas, mata kita jauh lebih peka terhadap variasi 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 artikel dari creative JS ini untuk mengetahui info selengkapnya, tetapi ini adalah langkah pertama yang penting untuk 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 Anda memerlukan waktu lebih dari 16 milidetik untuk berjalan, 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 melebihi anggaran frame selama callback requestAnimationFrame.

Membuka linimasa Dev Tools dan merekam animasi ini saat beraksi dengan cepat menunjukkan bahwa kita jauh melebihi anggaran saat menganimasikan. Di Linimasa, beralihlah ke "Frames" dan lihat:

Demo dengan terlalu banyak tata letak
Demo dengan terlalu banyak tata letak

Callback requestAnimationFrame (rAF) tersebut memerlukan waktu >200 md. Waktu tersebut terlalu lama untuk menampilkan frame setiap 16 milidetik. Membuka salah satu callback rAF yang panjang tersebut akan mengungkapkan apa yang terjadi di dalamnya: dalam hal ini, banyak tata letak.

Video Paul membahas lebih detail tentang penyebab spesifik tata letak ulang (membaca scrollTop) dan cara menghindarinya. Namun, intinya di sini adalah Anda dapat mempelajari callback dan menyelidiki apa yang memerlukan waktu lama.

Demo yang diperbarui dengan tata letak yang jauh lebih sederhana
Demo yang diperbarui dengan tata letak yang jauh lebih ringkas

Perhatikan waktu render frame 16 md. Ruang kosong dalam frame tersebut adalah ruang yang Anda miliki untuk melakukan lebih banyak pekerjaan (atau membiarkan browser melakukan pekerjaan yang perlu dilakukan di latar belakang). Ruang kosong tersebut adalah Hal yang Baik.

Sumber jank lainnya

Penyebab masalah terbesar saat mencoba menjalankan animasi yang didukung JavaScript adalah hal-hal lain dapat menghalangi callback rAF Anda, dan bahkan mencegahnya berjalan sama sekali. Meskipun callback rAF Anda ringan dan berjalan hanya dalam beberapa milidetik, aktivitas lain (seperti memproses XHR yang baru masuk, menjalankan pengendali peristiwa input, atau menjalankan update terjadwal pada timer) dapat tiba-tiba masuk dan berjalan selama jangka waktu tertentu tanpa menghasilkan apa pun. Di perangkat seluler, terkadang pemrosesan peristiwa ini dapat memerlukan waktu ratusan milidetik, dan selama waktu tersebut, animasi Anda akan benar-benar terhenti. Kami menyebut gangguan animasi tersebut sebagai jank.

Tidak ada solusi ajaib untuk menghindari situasi ini, tetapi ada beberapa praktik terbaik arsitektur untuk menyiapkan diri 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 umum dari jankiness yang mengerikan.
  • Dorong sebanyak mungkin pemrosesan (baca: apa pun yang akan memerlukan waktu lama untuk dijalankan) ke callback rAF atau Web Worker Anda.
  • Jika Anda memasukkan pekerjaan ke dalam callback rAF, cobalah untuk membaginya 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 menganimasi dengan lancar.

Untuk tutorial bagus yang membahas cara mendorong pemrosesan ke callback requestAnimationFrame, bukan pengendali input, lihat artikel Paul Lewis Leaner, Meaner, Faster Animations with requestAnimationFrame.

Animasi CSS

Apa yang lebih baik daripada JS ringan dalam callback peristiwa dan rAF Anda? No JS.

Sebelumnya, kami telah menyampaikan bahwa tidak ada solusi instan untuk menghindari gangguan pada callback rAF, tetapi Anda dapat menggunakan animasi CSS untuk menghindari kebutuhan akan callback rAF sepenuhnya. Khususnya di Chrome untuk Android (dan browser lain sedang mengembangkan fitur serupa), animasi CSS memiliki properti yang sangat diinginkan, yaitu browser sering kali 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: 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 segera di Chrome desktop, meskipun belum).

Jika memungkinkan, penggunaan animasi CSS akan menyederhanakan aplikasi Anda dan memungkinkan animasi berjalan 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 menggerakkan animasi tersebut dengan animasi CSS, jank tidak akan terjadi lagi.

(Ingat 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 mengetahui informasi selengkapnya tentang penggunaan Animasi CSS, lihat artikel seperti artikel ini di MDN.

Penutup

Singkatnya:

  1. Saat menganimasikan, pembuatan frame untuk setiap refresh layar sangatlah penting. Animasi yang disinkronkan dengan Vsync memberikan dampak positif yang besar pada cara aplikasi terasa.
  2. Cara terbaik untuk mendapatkan animasi yang disinkronkan dengan vsync di Chrome dan browser modern lainnya adalah dengan menggunakan animasi CSS. Jika Anda memerlukan fleksibilitas yang lebih besar daripada yang disediakan animasi CSS, teknik terbaik adalah animasi berbasis requestAnimationFrame.
  3. Agar animasi rAF tetap berjalan lancar, pastikan pengendali peristiwa lain tidak menghalangi callback rAF Anda berjalan, dan buat callback rAF tetap singkat (<15 md).

Terakhir, animasi yang disinkronkan dengan 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 ini.

Selamat membuat animasi!

Referensi