Mengoptimalkan animasi aplikasi web Anda
Singkatnya: Animation Worklet memungkinkan Anda menulis animasi imperatif yang berjalan pada kecepatan frame native perangkat untuk kehalusan bebas jank yang sangat mulus™, membuat animasi Anda lebih tangguh terhadap jank thread utama dan dapat ditautkan ke scroll, bukan waktu. Animation Worklet ada di Chrome Canary (di balik tanda "Fitur Platform Web Eksperimental") dan kami merencanakan Uji Coba Origin untuk Chrome 71. Anda dapat mulai menggunakannya sebagai progressive enhancement hari ini.
API Animasi Lain?
Sebenarnya tidak, ini adalah perluasan dari apa yang sudah kita miliki, dan dengan alasan yang baik. Mari kita mulai dari awal. Jika Anda ingin menganimasikan elemen DOM apa pun di web saat ini, Anda memiliki 2 ½ pilihan: Transisi CSS untuk transisi sederhana dari A ke B, Animasi CSS untuk animasi berbasis waktu yang berpotensi siklik dan lebih kompleks, serta Web Animations API (WAAPI) untuk animasi yang hampir kompleks secara arbitrer. Matriks dukungan WAAPI terlihat cukup suram, tetapi trennya meningkat. Sebelum itu, ada polyfill.
Kesamaan semua metode ini adalah bahwa metode ini tidak memiliki status dan didorong oleh waktu. Namun, beberapa efek yang dicoba developer tidak didorong oleh waktu dan tidak stateless. Misalnya, penggeser paralaks yang terkenal, seperti namanya, digerakkan oleh scroll. Menerapkan penggeser paralaks berperforma tinggi di web saat ini ternyata sulit.
Bagaimana dengan status stateless? Misalnya, pikirkan kolom URL Chrome di Android. Jika Anda men-scroll ke bawah, widget akan keluar dari tampilan. Namun, saat Anda men-scroll ke atas, header akan muncul kembali, meskipun Anda berada di tengah-tengah halaman tersebut. Animasi tidak hanya bergantung pada posisi scroll, tetapi juga pada arah scroll Anda sebelumnya. Aplikasi ini stateful.
Masalah lainnya adalah menata gaya scrollbar. Elemen ini terkenal tidak dapat distilisasi — atau setidaknya tidak cukup dapat distilisasi. Bagaimana jika saya ingin nyan cat sebagai scrollbar saya? Teknik apa pun yang Anda pilih, membuat scrollbar kustom tidaklah berperforma, maupun mudah.
Intinya, semua hal ini canggung dan sulit, bahkan tidak mungkin diterapkan secara efisien. Sebagian besar mengandalkan peristiwa dan/atau
requestAnimationFrame
, yang dapat membuat Anda tetap berada di 60 fps, meskipun layar Anda
dapat berjalan pada 90 fps, 120 fps, atau lebih tinggi dan menggunakan sebagian kecil
anggaran frame thread utama yang berharga.
Worklet Animasi memperluas kemampuan stack animasi web untuk mempermudah efek semacam ini. Sebelum kita mulai, pastikan kita memahami dasar-dasar animasi.
Pengantar tentang animasi dan linimasa
WAAPI dan Animation Worklet menggunakan linimasa secara ekstensif untuk memungkinkan Anda mengatur animasi dan efek sesuai keinginan. Bagian ini adalah pengingat cepat atau pengantar tentang linimasa dan cara kerjanya dengan animasi.
Setiap dokumen memiliki document.timeline
. Dimulai dari 0 saat dokumen dibuat dan menghitung milidetik sejak dokumen mulai ada. Semua animasi dokumen berfungsi relatif terhadap linimasa ini.
Agar lebih jelas, mari kita lihat cuplikan WAAPI ini
const animation = new Animation(
new KeyframeEffect(
document.querySelector('#a'),
[
{
transform: 'translateX(0)',
},
{
transform: 'translateX(500px)',
},
{
transform: 'translateY(500px)',
},
],
{
delay: 3000,
duration: 2000,
iterations: 3,
}
),
document.timeline
);
animation.play();
Saat kita memanggil animation.play()
, animasi menggunakan currentTime
linimasa sebagai waktu mulainya. Animasi kita memiliki penundaan 3000 md, yang berarti animasi akan dimulai (atau menjadi "aktif") saat linimasa mencapai `startTime
- 3000
. After that time, the animation engine will animate the given element from the first keyframe (
translateX(0)), through all intermediate keyframes (
translateX(500px)) all the way to the last keyframe (
translateY(500px)) in exactly 2000ms, as prescribed by the
durationoptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline's
currentTimeis
startTime + 3000 + 1000and the last keyframe at
startTime + 3000 + 2000`. Intinya, garis waktu mengontrol posisi kita dalam animasi.
Setelah mencapai keyframe terakhir, animasi akan melompat kembali ke keyframe pertama dan memulai iterasi animasi berikutnya. Proses ini diulang sebanyak 3 kali sejak kita menetapkan iterations: 3
. Jika ingin animasi tidak pernah berhenti, kita akan menulis iterations: Number.POSITIVE_INFINITY
. Berikut
hasil kode
di atas.
WAAPI sangat canggih dan ada banyak lagi fitur dalam API ini seperti easing, offset awal, pembobotan keyframe, dan perilaku pengisian yang akan melampaui cakupan artikel ini. Jika Anda ingin mengetahui lebih lanjut, sebaiknya baca artikel tentang Animasi CSS di CSS Tricks ini.
Menulis Worklet Animasi
Setelah memahami konsep linimasa, kita dapat mulai melihat Animation Worklet dan cara kerjanya dalam memanipulasi linimasa. Animation Worklet API tidak hanya didasarkan pada WAAPI, tetapi — dalam arti web yang dapat di-extend — merupakan primitif tingkat rendah yang menjelaskan cara kerja WAAPI. Dari segi sintaksis, keduanya sangat mirip:
Worklet Animasi | WAAPI |
---|---|
new WorkletAnimation( 'passthrough', new KeyframeEffect( document.querySelector('#a'), [ { transform: 'translateX(0)' }, { transform: 'translateX(500px)' } ], { duration: 2000, iterations: Number.POSITIVE_INFINITY } ), document.timeline ).play(); |
new Animation( new KeyframeEffect( document.querySelector('#a'), [ { transform: 'translateX(0)' }, { transform: 'translateX(500px)' } ], { duration: 2000, iterations: Number.POSITIVE_INFINITY } ), document.timeline ).play(); |
Perbedaannya ada pada parameter pertama, yaitu nama worklet yang mendorong animasi ini.
Deteksi fitur
Chrome adalah browser pertama yang meluncurkan fitur ini, jadi Anda harus memastikan kode Anda tidak hanya mengharapkan AnimationWorklet
ada. Jadi, sebelum memuat
worklet, kita harus mendeteksi apakah browser pengguna mendukung
AnimationWorklet
dengan pemeriksaan sederhana:
if ('animationWorklet' in CSS) {
// AnimationWorklet is supported!
}
Memuat worklet
Worklet adalah konsep baru yang diperkenalkan oleh gugus tugas Houdini untuk mempermudah pembuatan dan penskalaan banyak API baru. Kita akan membahas detail worklet lebih lanjut nanti, tetapi untuk mempermudah, Anda dapat menganggapnya sebagai thread yang murah dan ringan (seperti pekerja) untuk saat ini.
Kita harus memastikan bahwa kita telah memuat worklet dengan nama "passthrough", sebelum mendeklarasikan animasi:
// index.html
await CSS.animationWorklet.addModule('passthrough-aw.js');
// ... WorkletAnimation initialization from above ...
// passthrough-aw.js
registerAnimator(
'passthrough',
class {
animate(currentTime, effect) {
effect.localTime = currentTime;
}
}
);
Apa yang terjadi di sini? Kita mendaftarkan class sebagai animator menggunakan
panggilan registerAnimator()
AnimationWorklet, yang memberinya nama "passthrough".
Nama ini sama dengan yang kita gunakan di konstruktor WorkletAnimation()
di atas. Setelah
pendaftaran selesai, promise yang ditampilkan oleh addModule()
akan diselesaikan dan
kita dapat mulai membuat animasi menggunakan worklet tersebut.
Metode animate()
instance kita akan dipanggil untuk setiap frame yang ingin dirender browser, dengan meneruskan currentTime
linimasa animasi serta efek yang sedang diproses. Kita hanya memiliki satu
efek, yaitu KeyframeEffect
, dan kita menggunakan currentTime
untuk menyetel
localTime
efek, sehingga animator ini disebut "passthrough". Dengan kode ini untuk
worklet, WAAPI dan AnimationWorklet di atas berperilaku sama persis, seperti yang dapat Anda lihat di
demo.
Waktu
Parameter currentTime
dari metode animate()
adalah currentTime
dari
linimasa yang kita teruskan ke konstruktor WorkletAnimation()
. Dalam contoh
sebelumnya, kita hanya meneruskan waktu tersebut ke efek. Namun, karena ini adalah
kode JavaScript, kita dapat memutarbalikkan waktu 💫
function remap(minIn, maxIn, minOut, maxOut, v) {
return ((v - minIn) / (maxIn - minIn)) * (maxOut - minOut) + minOut;
}
registerAnimator(
'sin',
class {
animate(currentTime, effect) {
effect.localTime = remap(
-1,
1,
0,
2000,
Math.sin((currentTime * 2 * Math.PI) / 2000)
);
}
}
);
Kita mengambil Math.sin()
dari currentTime
, dan memetakan ulang nilai tersebut ke rentang [0; 2000], yang merupakan rentang waktu yang ditentukan untuk efek kita. Sekarang
animasinya terlihat sangat berbeda, tanpa
mengubah keyframe atau opsi animasi. Kode worklet dapat
sangat kompleks, dan memungkinkan Anda menentukan secara terprogram efek mana yang
dimainkan dalam urutan dan tingkat yang mana.
Opsi di atas Opsi
Anda mungkin ingin menggunakan kembali worklet dan mengubah angkanya. Oleh karena itu, konstruktor WorkletAnimation memungkinkan Anda meneruskan objek opsi ke worklet:
registerAnimator(
'factor',
class {
constructor(options = {}) {
this.factor = options.factor || 1;
}
animate(currentTime, effect) {
effect.localTime = currentTime * this.factor;
}
}
);
new WorkletAnimation(
'factor',
new KeyframeEffect(
document.querySelector('#b'),
[
/* ... same keyframes as before ... */
],
{
duration: 2000,
iterations: Number.POSITIVE_INFINITY,
}
),
document.timeline,
{factor: 0.5}
).play();
Dalam contoh ini, kedua animasi didorong dengan kode yang sama, tetapi dengan opsi yang berbeda.
Berikan negara bagian Anda!
Seperti yang saya sebutkan sebelumnya, salah satu masalah utama yang ingin dipecahkan oleh worklet animasi adalah animasi stateful. Worklet animasi diizinkan untuk menyimpan status. Namun, salah satu fitur inti worklet adalah bahwa worklet dapat dimigrasikan ke thread lain atau bahkan dihancurkan untuk menghemat resource, yang juga akan menghancurkan statusnya. Untuk mencegah hilangnya status, worklet animasi menawarkan hook yang
dipanggil sebelum worklet dihancurkan yang dapat Anda gunakan untuk menampilkan objek
status. Objek tersebut akan diteruskan ke konstruktor saat worklet
dibuat ulang. Saat pembuatan awal, parameter tersebut akan menjadi undefined
.
registerAnimator(
'randomspin',
class {
constructor(options = {}, state = {}) {
this.direction = state.direction || (Math.random() > 0.5 ? 1 : -1);
}
animate(currentTime, effect) {
// Some math to make sure that `localTime` is always > 0.
effect.localTime = 2000 + this.direction * (currentTime % 2000);
}
destroy() {
return {
direction: this.direction,
};
}
}
);
Setiap kali Anda memuat ulang demo ini, Anda memiliki peluang 50/50
ke arah mana persegi akan berputar. Jika browser menghancurkan worklet dan memigrasikannya ke thread lain, akan ada panggilan Math.random()
lain saat pembuatan, yang dapat menyebabkan perubahan arah yang tiba-tiba. Untuk memastikan hal itu tidak terjadi, kita menampilkan arah yang dipilih secara acak untuk animasi sebagai state dan menggunakannya di konstruktor, jika disediakan.
Menghubungkan ke kontinuum ruang-waktu: ScrollTimeline
Seperti yang ditunjukkan di bagian sebelumnya, AnimationWorklet memungkinkan kita
menentukan secara terprogram bagaimana memajukan linimasa memengaruhi efek
animasi. Namun, sejauh ini, linimasa kita selalu document.timeline
, yang
melacak waktu.
ScrollTimeline
membuka kemungkinan baru dan memungkinkan Anda mendorong animasi dengan scrolling, bukan waktu. Kita akan menggunakan kembali worklet "passthrough" pertama
untuk demo ini:
new WorkletAnimation(
'passthrough',
new KeyframeEffect(
document.querySelector('#a'),
[
{
transform: 'translateX(0)',
},
{
transform: 'translateX(500px)',
},
],
{
duration: 2000,
fill: 'both',
}
),
new ScrollTimeline({
scrollSource: document.querySelector('main'),
orientation: 'vertical', // "horizontal" or "vertical".
timeRange: 2000,
})
).play();
Daripada meneruskan document.timeline
, kita membuat ScrollTimeline
baru.
Anda mungkin sudah menebaknya, ScrollTimeline
tidak menggunakan waktu, tetapi posisi scroll scrollSource
untuk menyetel currentTime
di worklet. Jika di-scroll hingga ke atas (atau kiri), berarti currentTime = 0
, sedangkan jika di-scroll hingga ke bawah (atau kanan), currentTime
akan ditetapkan ke timeRange
. Jika men-scroll kotak dalam
demo ini, Anda dapat
mengontrol posisi kotak merah.
Jika Anda membuat ScrollTimeline
dengan elemen yang tidak dapat di-scroll, currentTime
pada linimasa akan menjadi NaN
. Jadi, terutama dengan mempertimbangkan desain responsif, Anda harus selalu siap untuk NaN
sebagai currentTime
Anda. Biasanya, nilai default yang masuk akal adalah 0.
Menautkan animasi dengan posisi scroll adalah sesuatu yang telah lama dicari, tetapi tidak pernah benar-benar dicapai pada tingkat keakuratan ini (selain solusi sementara yang tidak praktis dengan CSS3D). Worklet Animasi memungkinkan efek ini diterapkan dengan cara yang mudah dan berperforma tinggi. Misalnya: efek scrolling paralaks seperti demo ini menunjukkan bahwa sekarang hanya perlu beberapa baris untuk menentukan animasi yang digerakkan scroll.
Di balik layar
Worklet
Worklet adalah konteks JavaScript dengan cakupan terisolasi dan permukaan API yang sangat kecil. Permukaan API kecil memungkinkan pengoptimalan yang lebih agresif dari browser, terutama pada perangkat kelas bawah. Selain itu, worklet tidak terikat ke loop peristiwa tertentu, tetapi dapat dipindahkan antar-thread sesuai kebutuhan. Hal ini sangat penting untuk AnimationWorklet.
NSync Kompositor
Anda mungkin tahu bahwa properti CSS tertentu dapat dianimasikan dengan cepat, sementara yang lain tidak. Beberapa properti hanya memerlukan beberapa pekerjaan di GPU untuk dianimasikan, sementara properti lainnya memaksa browser untuk menata ulang seluruh dokumen.
Di Chrome (seperti di banyak browser lainnya), kami memiliki proses yang disebut compositor, yang tugasnya — dan di sini saya menyederhanakannya — adalah mengatur lapisan dan tekstur, lalu memanfaatkan GPU untuk memperbarui layar sesering mungkin, idealnya secepat layar dapat diperbarui (biasanya 60 Hz). Bergantung pada properti CSS yang dianimasikan, browser mungkin hanya perlu membuat kompositor melakukan tugasnya, sementara properti lain perlu menjalankan tata letak, yang merupakan operasi yang hanya dapat dilakukan oleh thread utama. Bergantung pada properti yang akan dianimasikan, worklet animasi Anda akan terikat ke thread utama atau berjalan di thread terpisah yang disinkronkan dengan kompositor.
Teguran ringan
Biasanya hanya ada satu proses compositor yang berpotensi digunakan bersama di beberapa tab, karena GPU adalah resource yang sangat diperebutkan. Jika compositor entah bagaimana diblokir, seluruh browser akan berhenti dan tidak responsif terhadap input pengguna. Hal ini harus dihindari dengan cara apa pun. Jadi, apa yang terjadi jika worklet Anda tidak dapat mengirimkan data yang dibutuhkan compositor tepat waktu agar frame dapat dirender?
Jika hal ini terjadi, worklet diizinkan — sesuai spesifikasi — untuk "tergelincir". Hal ini tertinggal dari compositor, dan compositor diizinkan untuk menggunakan kembali data frame terakhir untuk mempertahankan kecepatan frame. Secara visual, hal ini akan terlihat seperti jank, tetapi perbedaan besarnya adalah browser masih responsif terhadap input pengguna.
Kesimpulan
Ada banyak aspek AnimationWorklet dan manfaat yang diberikannya untuk web. Manfaat yang jelas adalah kontrol yang lebih besar terhadap animasi dan cara baru untuk mendorong animasi guna menghadirkan tingkat kesetiaan visual baru ke web. Namun, desain API juga memungkinkan Anda membuat aplikasi lebih tangguh terhadap jank sekaligus mendapatkan akses ke semua fitur baru secara bersamaan.
Worklet Animasi ada di Canary dan kami menargetkan Uji Coba Origin dengan Chrome 71. Kami sangat menantikan pengalaman web baru Anda yang luar biasa dan ingin mendengar masukan tentang apa yang dapat kami tingkatkan. Ada juga polyfill yang memberi Anda API yang sama, tetapi tidak memberikan isolasi performa.
Perlu diingat bahwa Transisi CSS dan Animasi CSS masih merupakan opsi yang valid dan bisa jauh lebih sederhana untuk animasi dasar. Namun, jika Anda ingin menggunakan animasi yang lebih canggih, AnimationWorklet siap membantu Anda.