Jank busting untuk performa rendering yang lebih baik

Tom Wiltzius
Tom Wiltzius

Pengantar

Anda ingin aplikasi web terasa responsif dan halus saat membuat animasi, transisi, dan efek UI kecil lainnya. Memastikan efek ini bebas jank dapat menunjukkan perbedaan antara nuansa "asli" atau nuansa yang kaku dan tidak rapi.

Ini adalah yang pertama dari rangkaian artikel yang membahas pengoptimalan performa rendering di browser. Untuk memulai, kita akan membahas mengapa animasi yang lancar itu sulit dan apa yang perlu dilakukan untuk mencapainya, serta beberapa praktik terbaik yang mudah. Banyak dari ide ini yang mulanya disampaikan dalam "Jank Busters", sebuah diskusi yang disampaikan oleh Nat Duca dan saya hadirkan di acara Google I/O (video) tahun ini.

Memperkenalkan V-sync

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

Pertimbangkan tampilan ponsel: layar diperbarui dengan interval yang teratur, biasanya (tetapi tidak selalu!) sekitar 60 kali per detik. V-sync (atau sinkronisasi vertikal) mengacu pada praktik menghasilkan bingkai baru hanya di antara penyegaran layar. Anda mungkin menganggap ini seperti kondisi race antara proses yang menulis data ke dalam {i>buffer<i} layar dan sistem operasi yang membaca data tersebut untuk meletakkannya 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 frame lainnya, sehingga menyebabkan "tearing".

Untuk mendapatkan animasi yang lancar, Anda harus menyiapkan bingkai baru setiap kali terjadi pemuatan ulang layar. Hal ini memiliki dua implikasi besar: waktu render frame (yaitu, saat frame harus disiapkan) 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 memproduksi 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. Ini menjadi masalah karena berbagai alasan (dan kami akan membahasnya lebih lanjut dalam satu menit), tetapi yang perlu dikhawatirkan adalah:

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

Ingat kembali masalah pengaturan waktu render frame yang disebutkan di atas: Anda membutuhkan frame animasi yang telah selesai, yang sudah selesai 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 layar berikutnya dimuat ulang, tetapi variasi dalam kecepatan refresh layar membuatnya tidak mungkin dilakukan dengan timer tetap. Tidak peduli berapa interval timer, Anda akan perlahan-lahan keluar dari periode waktu untuk sebuah frame dan akhirnya melepaskannya. Hal ini akan terjadi meskipun timer dipicu dengan akurasi milidetik, yang tidak akan demikian (seperti yang telah ditemukan oleh developer) -- resolusi timer bervariasi, bergantung pada apakah mesin sedang menggunakan baterai vs. dicolokkan, dapat dipengaruhi oleh tab latar belakang yang meniru resource, dll. Meskipun ini jarang terjadi (misalnya, setiap 16 frame karena Anda nonaktif dalam hitungan milidetik), Anda akan melihat: Anda akan mengalami beberapa frame dalam satu detik. Anda juga akan melakukan pekerjaan untuk menghasilkan frame yang tidak pernah ditampilkan, yang menghabiskan daya dan waktu CPU yang dapat Anda habiskan untuk melakukan hal-hal lain di aplikasi.

Layar yang berbeda memiliki kecepatan refresh yang berbeda: 60 Hz biasa digunakan, tetapi beberapa ponsel 59 Hz, beberapa laptop turun ke 50 Hz dalam mode daya rendah, beberapa monitor desktop 70 Hz.

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

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

requestAnimationFrame juga memiliki properti bagus lainnya:

  • Animasi di tab latar belakang dijeda, menghemat resource sistem dan masa pakai baterai.
  • Jika sistem tidak dapat menangani rendering pada kecepatan refresh layar, sistem dapat men-throttle animasi dan menghasilkan callback lebih jarang (misalnya, 30 kali per detik pada layar 60 Hz). Meskipun menurunkan kecepatan frame menjadi setengahnya, hal ini membuat animasi tetap konsisten -- dan seperti yang disebutkan di atas, mata kita jauh lebih disesuaikan dengan varians daripada kecepatan frame. 30 Hz stabil terlihat lebih baik daripada 60 Hz yang melewatkan beberapa frame per detik.

requestAnimationFrame sudah dibahas di semua tempat, jadi baca artikel seperti yang satu ini dari JS materi iklan untuk info selengkapnya tentang hal ini, tetapi langkah pertama yang penting untuk mewujudkan animasi yang lancar.

Buat Anggaran

Karena kita ingin frame baru siap setiap kali layar dimuat ulang, maka hanya ada waktu di sela-sela refresh untuk melakukan semua tugas membuat frame baru. Pada tampilan 60Hz, itu berarti kita memerlukan waktu sekitar 16 md untuk menjalankan semua JavaScript, melakukan layout, paint, dan apa pun yang harus dilakukan browser untuk mengeluarkan bingkai. Artinya, jika JavaScript di dalam callback requestAnimationFrame Anda membutuhkan waktu lebih dari 16 md untuk dijalankan, Anda tidak dapat membuat frame tepat waktu untuk v-sync.

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

Membuka linimasa Dev Tools dan merekam animasi ini menunjukkan dengan cepat bahwa kita sudah melebihi anggaran saat membuat animasi. Di Linimasa, beralihlah ke "Bingkai" 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 lama untuk menandai frame setiap 16 md. Membuka salah satu callback rAF yang panjang itu akan mengungkapkan apa yang terjadi di dalamnya: dalam hal ini, banyak tata letak.

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

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

Perhatikan waktu render frame 16 md. Ruang kosong pada bingkai tersebut adalah ruang utama yang harus Anda lakukan untuk melakukan lebih banyak pekerjaan (atau membiarkan browser melakukan pekerjaan yang perlu dilakukan di latar belakang). Spasi kosong itu adalah suatu hal yang baik.

Sumber Lain Jank

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 ramping dan berjalan hanya dalam beberapa milidetik, aktivitas lain (seperti memproses XHR yang baru masuk, menjalankan pengendali peristiwa input, atau menjalankan update terjadwal pada timer) tiba-tiba bisa masuk dan berjalan untuk jangka waktu tertentu tanpa menghasilkan callback. Di perangkat seluler, terkadang pemrosesan peristiwa ini dapat memerlukan waktu ratusan milidetik, dan selama itu animasi Anda akan terhenti sepenuhnya. Kita menyebut animasi itu disebut jank.

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

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

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

Animasi CSS

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

Sebelumnya, kami mengatakan bahwa tidak ada solusi praktis untuk menghindari interupsi pada callback rAF, tetapi Anda dapat menggunakan animasi CSS agar tidak diperlukan sama sekali. Khususnya pada Chrome untuk Android (dan browser lain sedang mengerjakan fitur serupa), animasi CSS memiliki properti yang sangat diinginkan sehingga browser sering kali dapat menjalankannya meskipun JavaScript sedang berjalan.

Ada pernyataan implisit di bagian atas terkait jank: browser hanya dapat melakukan satu hal dalam satu waktu. Hal ini tidak sepenuhnya benar, namun akan lebih baik jika Anda memiliki: pada waktu tertentu, browser dapat menjalankan JS, melakukan 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 membuat animasi berjalan lancar, sekalipun 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. Namun, jika kita menggerakkan animasi tersebut dengan animasi CSS, jank tidak lagi terjadi.

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

Rangkuman

Singkatnya adalah:

  1. Saat menganimasikan, menghasilkan frame untuk setiap refresh layar itu penting. Animasi Vsync memberikan dampak positif yang besar pada perasaan aplikasi.
  2. Cara terbaik untuk mendapatkan animasi vsync di Chrome dan browser modern lainnya adalah dengan menggunakan animasi CSS. Saat Anda membutuhkan lebih banyak fleksibilitas daripada yang disediakan animasi CSS, teknik terbaik adalah animasi berbasis requestAnimationFrame.
  3. Untuk menjaga animasi rAF tetap sehat dan menyenangkan, pastikan pengendali peristiwa lainnya tidak menghalangi callback rAF Anda berjalan, dan pertahankan callback rAF singkat (<15 md).

Terakhir, animasi vsync'd tidak hanya berlaku untuk animasi UI sederhana - ini berlaku untuk animasi Canvas2D, animasi WebGL, dan bahkan menggulir pada laman statis. Di artikel berikutnya dalam rangkaian ini, kita akan mempelajari performa scroll dengan mempertimbangkan konsep ini.

Selamat beranimasi!

Referensi