Jank busting untuk performa rendering yang lebih baik

Tom Wiltzius
Tom Wiltzius

Pengantar

Anda ingin aplikasi web terasa responsif dan mulus saat melakukan animasi, transisi, dan efek UI kecil lainnya. Memastikan efek ini bebas dari jank dapat berarti perbedaan antara "native" atau terasa kaku dan tidak rapi.

Artikel ini adalah yang pertama dari rangkaian artikel yang membahas pengoptimalan performa rendering di browser. Untuk memulainya, kami akan membahas mengapa animasi yang mulus itu sulit dan apa yang harus dilakukan untuk mencapainya, serta beberapa praktik terbaik yang mudah. Banyak dari ide ini yang awalnya disajikan dalam "Jank Busters", perbincangan Nat Duca dan saya di Google I/O talk (video) tahun ini.

Memperkenalkan V-sync

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

Pertimbangkan layar ponsel Anda: tampilan ponsel diperbarui dalam interval yang teratur, biasanya (tetapi tidak selalu!) sekitar 60 kali per detik. {i>V-sync<i} (atau sinkronisasi vertikal) mengacu pada praktik menghasilkan bingkai baru hanya di antara penyegaran layar. Anda mungkin menganggap ini seperti kondisi perlombaan antara proses yang menulis data ke dalam buffer layar dan sistem operasi yang membaca data tersebut untuk meletakkannya di layar. Kita ingin konten {i>buffer<i} {i>frame<i} berubah di antara penyegaran ini, bukan selama pembaruan; jika tidak, monitor akan menampilkan setengah dari satu dan setengah frame lainnya, yang menyebabkan "tearing".

Untuk mendapatkan animasi yang lancar, Anda perlu membuat frame baru setiap kali pemuatan ulang layar. Hal ini memiliki dua implikasi besar: pengaturan waktu render frame (yaitu, kapan frame harus disiapkan) dan anggaran frame (yaitu, berapa lama browser harus menghasilkan frame). Anda hanya memiliki waktu antara refresh layar untuk menyelesaikan sebuah frame (~16 md pada layar 60 Hz), dan Anda ingin mulai memproduksi frame berikutnya segera setelah yang terakhir ditampilkan di layar.

Pengaturan Waktu Semua: requestAnimationFrame

Banyak developer web menggunakan setInterval atau setTimeout setiap 16 milidetik untuk membuat animasi. Ini adalah masalah karena berbagai alasan (dan kami akan membahasnya beberapa saat lagi), namun yang perlu kami perhatikan adalah:

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

Ingat kembali masalah pengaturan waktu render frame yang disebutkan di atas: Anda memerlukan bingkai animasi yang telah selesai, diselesaikan dengan JavaScript, manipulasi DOM, tata letak, penggambaran, dll., agar siap sebelum pemuatan ulang layar berikutnya. Resolusi timer yang rendah dapat menyulitkan penyelesaian frame animasi sebelum refresh layar berikutnya, tetapi variasi kecepatan refresh layar tidak memungkinkan dengan timer yang tetap. Tidak peduli berapa interval timer-nya, Anda akan perlahan-lahan keluar dari jendela pengaturan waktu untuk sebuah frame dan akhirnya menjatuhkannya. Hal ini akan terjadi meskipun timer diaktifkan dengan akurasi milidetik, yang tidak akan terjadi (seperti yang diketahui developer) -- resolusi timer bervariasi bergantung pada apakah mesin sedang menggunakan baterai atau dicolokkan, dapat dipengaruhi oleh tab latar belakang yang menyedot resource, dll. Meskipun ini jarang terjadi (misalnya, setiap 16 frame karena Anda terputus per milidetik), Anda akan melihat: Anda akan menurunkan beberapa frame per detik. Anda juga akan melakukan pekerjaan untuk menghasilkan frame yang tidak pernah ditampilkan, yang menyia-nyiakan daya dan waktu CPU yang dapat Anda habiskan untuk melakukan hal-hal lain di aplikasi Anda.

Layar yang berbeda memiliki kecepatan refresh yang berbeda: 60Hz adalah hal yang umum, tetapi beberapa ponsel memiliki 59Hz, beberapa laptop turun hingga 50Hz dalam mode daya rendah, beberapa monitor desktop berukuran 70Hz.

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

Cara untuk mendapatkan frame animasi yang diatur waktunya dengan benar 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 akan dijeda, sehingga menghemat resource sistem dan masa pakai baterai.
  • Jika sistem tidak dapat menangani rendering pada kecepatan refresh layar, sistem dapat men-throttle animasi dan lebih jarang menghasilkan callback (misalnya, 30 kali per detik pada layar 60 Hz). Meskipun kecepatan frame ini turun setengahnya, animasinya tetap konsisten -- dan seperti yang disebutkan di atas, mata kita jauh lebih selaras dengan varians 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 baca artikel seperti yang ini dari JS materi iklan untuk mengetahui info selengkapnya tentang hal ini, tetapi ini merupakan langkah pertama yang penting untuk memperlancar animasi.

Bingkai Anggaran

Karena kita ingin frame baru setiap kali refresh layar, hanya ada waktu di sela-sela refresh untuk melakukan semua pekerjaan membuat frame baru. Pada layar 60 Hz, itu berarti kita punya waktu sekitar 16 md untuk menjalankan semua JavaScript, melakukan tata letak, menggambar, dan apa pun yang harus dilakukan browser untuk mengeluarkan bingkai. Artinya, jika JavaScript di dalam callback requestAnimationFrame membutuhkan waktu lebih dari 16 md untuk berjalan, Anda tidak perlu menghasilkan frame tepat waktu untuk v-sync.

16 md bukan waktu yang lama. Untungnya, Developer Tools Chrome dapat membantu melacak jika Anda menghabiskan anggaran frame selama callback requestAnimationFrame.

Membuka {i>timeline<i} Dev Tools dan melakukan perekaman animasi ini dengan cepat menunjukkan bahwa kita melebihi anggaran saat membuat animasi. Di Linimasa, beralihlah ke "Frame" dan lihat:

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

Callback requestAnimationFrame (rAF) tersebut memerlukan waktu >200 md. Itu urutan magnitudo yang terlalu panjang untuk menandai sebuah frame setiap 16 md! Membuka salah satu callback rAF yang panjang akan mengungkap apa yang terjadi di dalamnya: dalam hal ini, ada banyak tata letak.

Video Paul membahas lebih detail tentang penyebab spesifik dari tata letak ulang (bacaan scrollTop) dan cara menghindarinya. Namun, intinya di sini adalah Anda bisa menyelami callback dan menyelidiki apa yang butuh waktu begitu lama.

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

Perhatikan waktu render frame 16 md. Ruang kosong dalam bingkai adalah ruang utama yang Anda miliki untuk melakukan lebih banyak pekerjaan (atau membiarkan browser melakukan pekerjaan yang perlu dilakukannya di latar belakang). Ruang kosong itu adalah sesuatu yang baik.

Sumber Jank Lain

Penyebab terbesar dari masalah saat mencoba menjalankan animasi yang didukung JavaScript adalah bahwa hal-hal lain bisa menghalangi callback rAF Anda, dan bahkan mencegahnya agar tidak berjalan sama sekali. Bahkan jika callback rAF Anda ramping dan berjalan hanya dalam beberapa milidetik, aktivitas lainnya (seperti memproses XHR yang baru masuk, menjalankan pengendali peristiwa input, atau menjalankan pembaruan terjadwal pada timer) dapat tiba-tiba datang dan berjalan untuk periode waktu tertentu tanpa mengalah. Di perangkat seluler terkadang perangkat memproses peristiwa ini membutuhkan waktu ratusan milidetik, selama waktu tersebut animasi Anda akan benar-benar terhenti. Kita menyebutnya animasi mengalami jank.

Tidak ada solusi ajaib untuk menghindari situasi ini, tetapi ada beberapa praktik terbaik arsitektur untuk menyiapkan diri Anda meraih kesuksesan:

  • Jangan melakukan banyak pemrosesan dalam pengendali input. Melakukan banyak JS atau mencoba mengatur ulang seluruh halaman selama mis. pengendali onscroll adalah penyebab yang sangat umum dari jank yang mengerikan.
  • Dorong sebanyak mungkin pemrosesan (baca: apa pun yang akan memakan waktu lama untuk dijalankan) ke callback rAF atau Pekerja Web Anda.
  • Jika Anda mendorong pekerjaan ke callback rAF, cobalah untuk memotongnya sehingga Anda hanya memproses sedikit setiap frame atau menundanya hingga setelah animasi penting selesai -- dengan cara ini Anda dapat terus menjalankan callback rAF pendek dan menganimasikannya dengan halus.

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 dari JS ringan dalam peristiwa dan callback rAF Anda? Tanpa JS.

Sebelumnya kami mengatakan bahwa tidak ada solusi alternatif untuk menghindari gangguan callback rAF, tetapi Anda dapat menggunakan animasi CSS untuk menghindarinya sepenuhnya. Khususnya di Chrome untuk Android (dan browser lain sedang mengerjakan fitur serupa), animasi CSS memiliki properti yang sangat diinginkan sehingga browser sering dapat menjalankannya bahkan jika JavaScript sedang berjalan.

Ada pernyataan implisit di bagian atas tentang jank: browser hanya bisa melakukan satu hal dalam satu waktu. Hal ini tidak sepenuhnya benar, tetapi sebaiknya asumsikan: pada waktu tertentu browser dapat menjalankan JS, menjalankan tata letak, atau menggambar, tetapi hanya satu per satu. Hal ini dapat diverifikasi di tampilan Linimasa Dev Tools. Salah satu pengecualian untuk aturan ini adalah animasi CSS di Chrome untuk Android (dan segera 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, yang menyebabkan jank. Tetapi jika kita menjalankan animasi tersebut dengan animasi CSS, jank tidak akan terjadi lagi.

(Ingat 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 yang ini di MDN.

Akhir kata

Singkatnya,

  1. Saat membuat animasi, penting untuk menghasilkan frame untuk setiap refresh layar. Animasi vsync'd memberikan dampak positif yang besar pada perasaan aplikasi.
  2. Cara terbaik untuk mendapatkan animasi vsync'd di Chrome dan browser modern lainnya adalah untuk menggunakan animasi CSS. Saat Anda membutuhkan lebih banyak fleksibilitas daripada animasi CSS teknik terbaiknya adalah animasi berbasis requestAnimationFrame.
  3. Agar animasi rAF tetap berjalan dengan baik dan menyenangkan, pastikan pengendali peristiwa lainnya tidak menghalangi callback rAF berjalan, dan mempertahankan 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 pengguliran pada laman statis. Dalam artikel berikutnya dalam seri ini, kami akan membahas performa scroll dengan mempertimbangkan konsep ini.

Selamat membuat animasi!

Referensi