Meningkatkan kualitas animasi aplikasi web Anda
TL;DR: Worklet Animasi memungkinkan Anda menulis animasi imperatif yang berjalan pada kecepatan frame native perangkat untuk kelancaran bebas jank tanpa jank dari perangkat, membuat animasi lebih tahan terhadap jank pada thread utama dan dapat ditautkan men-scroll, bukan waktu. Worklet Animasi ada di Chrome Canary (di belakang "Fitur Platform Web eksperimental" flag) dan kami merencanakan Uji Coba Origin untuk Chrome 71. Anda dapat mulai menggunakannya sebagai {i>progressive enhancement<i} sekarang.
Animation API lain?
Sebenarnya tidak, ini adalah pengembangan dari apa yang sudah kita miliki, dan dengan alasan yang tepat! Mari kita mulai dari awal. Jika Anda ingin menganimasikan elemen DOM apa pun di web hari ini, Anda memiliki 2 1⁄2 pilihan: Transisi CSS untuk transisi A ke B sederhana, Animasi CSS untuk animasi berbasis waktu yang lebih bersifat siklus dan kompleks, serta Web Animations API (WAAPI) untuk animasi kompleks yang hampir sembarang bebas. Matriks dukungan WAAPI terlihat cukup suram, tetapi saat ini sedang naik. Sebelum itu, ada polyfill.
Kesamaan dari semua metode ini adalah metode tersebut stateless dan didorong oleh waktu. Tetapi beberapa efek yang coba dicoba oleh pengembang tidak berbasis waktu maupun stateless. Misalnya scroller paralaks yang terkenal adalah, karena nama menyiratkan, berbasis scroll. Mengimplementasikan scroller paralaks yang berperforma tinggi di web saat ini sangat sulit.
Dan bagaimana dengan stateless? Pikirkan tentang bilah alamat Chrome di Android, untuk contoh. Jika di-scroll ke bawah, layar akan ter-scroll hingga keluar. Tapi kedua Anda men-scroll ke atas, akan muncul kembali, meskipun Anda sudah setengah jalan ke bagian bawah halaman tersebut. Animasi tidak hanya bergantung pada posisi scroll, tetapi juga pada arah scroll Anda sebelumnya. Atribut ini bersifat stateful.
Masalah lainnya adalah penataan gaya scrollbar. Mereka terkenal tidak bergaya — atau sama setidaknya tidak cukup mudah untuk ditata. Bagaimana jika saya menginginkan kucingnyan sebagai scrollbar saya? Teknik apa pun yang Anda pilih, membuat scrollbar kustom bukanlah berperforma tinggi, atau mudah.
Intinya adalah semua hal ini canggung dan sulit untuk
menerapkannya secara efisien. Kebanyakan dari mereka
mengandalkan peristiwa dan/atau
requestAnimationFrame
, yang dapat mempertahankan kecepatan Anda pada 60 fps, bahkan saat layar
mampu berjalan pada 90 fps, 120 fps atau
lebih tinggi dan menggunakan sebagian kecil
anggaran {i>frame<i} thread utama yang berharga.
Worklet Animasi memperluas kemampuan tumpukan animasi web untuk membuat efek semacam ini lebih mudah. Sebelum kita mempelajari lebih lanjut, pastikan kita telah mengikuti perkembangan dasar-dasar animasi.
Pengantar tentang animasi dan linimasa
WAAPI dan Animation Worklet memanfaatkan linimasa secara ekstensif untuk memungkinkan Anda mengatur animasi dan efek sesuai keinginan Anda. Bagian ini adalah penyegaran singkat atau pengantar garis waktu dan bagaimana mereka bekerja dengan animasi.
Setiap dokumen memiliki document.timeline
. Dimulai dari 0 jika dokumen
dibuat dan menghitung jumlah milidetik
sejak dokumen mulai dibuat. Semua
animasi dokumen relatif terhadap garis waktu 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 akan menggunakan currentTime
linimasa
sebagai waktu mulainya. Animasi kita mengalami penundaan 3000 md, yang berarti bahwa
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 adalah, linimasa mengontrol posisi kita di animasi kita!
Setelah mencapai keyframe terakhir, animasi akan kembali ke keyframe pertama
dan memulai iterasi animasi berikutnya. Proses ini mengulangi
sebanyak 3 kali sejak kita menetapkan iterations: 3
. Jika kita ingin animasinya
tidak pernah berhenti, kita akan menulis iterations: Number.POSITIVE_INFINITY
. Berikut
hasil kode
di atas.
WAAPI sangat canggih dan masih banyak fitur lain dalam API ini seperti easing, offset memulai, pembobotan keyframe, dan perilaku pengisian yang akan meledakkan cakupan artikel ini. Jika Anda ingin mengetahui lebih lanjut, sebaiknya baca artikel tentang Animasi CSS di Trik CSS ini.
Menulis Worklet Animasi
Setelah kita memiliki konsep {i>timeline<i}, kita dapat mulai melihat Worklet Animasi dan caranya memungkinkan Anda mengotak-atik linimasa! Animasi Worklet API tidak hanya didasarkan pada WAAPI, melainkan — dalam artian web yang dapat diperluas — merupakan primitif tingkat rendah yang menjelaskan cara kerja WAAPI. Dalam hal sintaks, mereka 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 terletak pada parameter pertama, yang merupakan nama worklet yang menjalankan animasi ini.
Deteksi fitur
Chrome adalah browser pertama yang mengirimkan fitur ini, jadi Anda perlu memastikan
kode tidak hanya mengharapkan AnimationWorklet
untuk ada. Jadi, sebelum memuat
{i>worklet<i}, kita harus mendeteksi apakah {i>browser<i}
pengguna memiliki dukungan untuk
AnimationWorklet
dengan pemeriksaan sederhana:
if ('animationWorklet' in CSS) {
// AnimationWorklet is supported!
}
Memuat worklet
Worklet adalah konsep baru yang diperkenalkan oleh satuan tugas Houdini untuk membuat banyak API baru lebih mudah di-build dan diskalakan. Kita akan membahas detail {i>worklet<i} sedikit lagi nanti, tetapi untuk kesederhanaan, Anda dapat menganggapnya murah dan untuk saat ini, thread ringan (seperti pekerja).
Kita perlu memastikan bahwa kita telah memuat {i>worklet<i} dengan nama "{i>passthrough<i}", 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 sedang terjadi di sini? Kita mendaftarkan class sebagai animator menggunakan
Panggilan registerAnimator()
AnimationWorklet, memberinya nama "passthrough".
Nama ini sama dengan nama yang kita gunakan dalam konstruktor WorkletAnimation()
di atas. Setelah
pendaftaran selesai, promise yang ditampilkan oleh addModule()
akan di-resolve dan
kita dapat mulai membuat animasi
menggunakan {i>worklet<i} itu.
Metode animate()
dari instance kita akan dipanggil untuk setiap frame
browser ingin merender, dengan meneruskan currentTime
dari linimasa animasi
serta efek yang sedang diproses. Kita hanya punya satu
KeyframeEffect
, dan kita menggunakan currentTime
untuk menyetel efek
localTime
, sehingga animator ini disebut "passthrough". Dengan kode ini untuk
worklet, WAAPI, dan AnimationWorklet di atas berperilaku sama
sama, seperti yang terlihat di
demo.
Waktu
Parameter currentTime
dari metode animate()
kita adalah currentTime
dari
linimasa yang kita teruskan ke konstruktor WorkletAnimation()
. Di
contoh, kita baru saja meneruskan
waktu tersebut ke efek. Tapi karena ini adalah
kode JavaScript, dan kita dapat mengubah 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 untuk
rentang [0; 2000], yaitu rentang waktu di mana
efek kita didefinisikan. Baru saja
animasinya terlihat sangat berbeda, tanpa perlu
mengubah keyframe atau opsi animasi. Kode worklet dapat berupa
kompleks secara bebas, dan memungkinkan Anda untuk
menentukan secara terprogram efek mana
diputar dalam urutan apa dan sejauh mana.
Opsi daripada Opsi
Anda mungkin ingin menggunakan kembali worklet dan mengubah nomornya. 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 tersebut digerakkan dengan kode yang sama, tetapi dengan opsi yang berbeda.
Tampilkan negara bagian lokal Anda.
Seperti yang saya jelaskan sebelumnya, salah satu
masalah utama mengerjakan animasi adalah
animasi stateful. Worklet animasi diizinkan untuk menahan status. Namun, satu
fitur inti dari worklet adalah bahwa mereka dapat dimigrasikan ke
atau bahkan dihancurkan untuk menghemat sumber daya, yang juga akan menghancurkan
status. Untuk mencegah hilangnya status, worklet animasi menawarkan hook yang
dipanggil sebelum worklet dihancurkan yang dapat Anda gunakan untuk menampilkan status
. Objek itu akan diteruskan ke konstruktor saat worklet
dibuat ulang. Pada pembuatan awal, parameter tersebut adalah 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 memuat ulang demo ini, Anda mendapatkan skor 50/50
ke arah mana persegi akan berputar. Jika browser dihancurkan
{i>worklet<i} dan memigrasikannya ke
thread yang berbeda, akan ada
Math.random()
panggilan saat pembuatan, yang dapat menyebabkan perubahan tiba-tiba
arah. Untuk memastikan hal itu tidak terjadi, kita mengembalikan animasi
arah yang dipilih secara acak sebagai state dan menggunakannya dalam konstruktor, jika disediakan.
Terhubung ke kontinum ruang-waktu: ScrollLinimasa
Seperti yang telah ditunjukkan bagian sebelumnya, AnimationWorklet memungkinkan kita untuk
secara terprogram menentukan bagaimana kemajuan garis waktu akan memengaruhi efek
animasi. Namun sejauh ini, linimasa kami tetap document.timeline
, yaitu
melacak waktu.
ScrollTimeline
membuka kemungkinan baru dan memungkinkan Anda menjalankan animasi
dengan menggulir alih-alih waktu. Kita akan menggunakan kembali
"passthrough" worklet untuk ini
demo:
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();
Kita membuat ScrollTimeline
baru, bukan meneruskan document.timeline
.
Anda mungkin sudah menebaknya, ScrollTimeline
tidak menggunakan waktu tapi
Posisi scroll scrollSource
untuk menyetel currentTime
di worklet. Berada
di-scroll ke paling atas (atau ke kiri) berarti currentTime = 0
, sedangkan
di-scroll ke paling bawah (atau kanan) akan menyetel currentTime
ke
timeRange
. Jika Anda men-scroll kotak di
demo, Anda dapat
mengontrol posisi kotak merah.
Jika Anda membuat ScrollTimeline
dengan elemen yang tidak di-scroll,
currentTime
linimasa akan menjadi NaN
. Jadi, terutama dengan
desain responsif di
Anda harus selalu siap untuk NaN
sebagai currentTime
Anda. Sering
masuk akal untuk ditetapkan
secara {i>default<i} ke nilai 0.
Menautkan animasi dengan posisi scroll adalah sesuatu yang sudah lama dicari, tapi tidak pernah benar-benar dicapai pada tingkat {i>fidelity <i}ini (terlepas dari teknik solusi dengan CSS3D). Worklet Animasi memungkinkan efek ini diimplementasikan dengan mudah sekaligus berperforma tinggi. Contoh: efek scroll paralaks seperti ini demo menunjukkan bahwa hal tersebut sekarang hanya membutuhkan beberapa baris untuk menentukan animasi berbasis scroll.
Di balik layar
Worklet
Worklet adalah konteks JavaScript dengan cakupan yang terisolasi dan API yang sangat kecil ditampilkan. Platform API yang kecil memungkinkan pengoptimalan yang lebih agresif dari {i>browser<i} web, terutama pada perangkat kelas bawah. Selain itu, worklet tidak terikat pada loop peristiwa tertentu, tetapi bisa dipindahkan antar-thread sesuai kebutuhan. Ini adalah sangat penting untuk AnimationWorklet.
{i>Compositor NSync<i}
Anda mungkin tahu bahwa properti CSS tertentu cepat dianimasikan, sementara yang lain tidak. Beberapa properti hanya memerlukan beberapa pekerjaan di GPU untuk dianimasikan, sementara yang lain memaksa {i>browser<i} untuk mengatur ulang seluruh dokumen.
Di Chrome (seperti di banyak {i>browser<i} lain) kita memiliki proses yang disebut {i>compositor<i}, tugasnya — dan saya sangat menyederhanakannya — untuk mengatur {i>layer<i} dan tekstur dan kemudian memanfaatkan GPU untuk memperbarui layar secara teratur, idealnya secepat pembaruan layar (biasanya 60 Hz). Bergantung pada Properti CSS sedang dianimasikan, browser mungkin hanya perlu memiliki compositor melakukan tugasnya, sementara properti lain perlu menjalankan tata letak, yang merupakan operasi yang hanya dapat dilakukan oleh thread utama. Tergantung pada properti yang Anda berencana dianimasikan, {i>worklet<i} animasi Anda akan terikat ke elemen atau berjalan di utas terpisah yang disinkronkan dengan compositor.
Menampar di pergelangan tangan
Biasanya hanya ada satu proses {i>compositor<i} yang berpotensi dibagikan pada beberapa tab, karena GPU adalah sumber daya yang sangat bersaing. Jika compositor mendapat entah bagaimana diblokir, seluruh browser berhenti berfungsi dan menjadi tidak responsif terhadap input pengguna. Hal ini harus dihindari dengan segala cara. Jadi apa yang terjadi jika {i>worklet<i} tidak dapat mengirimkan data yang dibutuhkan compositor tepat waktu sebelum {i>frame<i} dirender?
Jika ini terjadi, worklet diizinkan — sesuai spesifikasi — untuk "tergelincir". Tertinggal compositor, dan compositor diizinkan untuk menggunakan kembali data dari {i>frame<i} terakhir untuk menjaga agar kecepatan frame tetap tinggi. Secara visual, ini akan terlihat seperti jank, tetapi perbedaannya adalah bahwa browser masih responsif terhadap input pengguna.
Kesimpulan
Ada banyak faset pada AnimationWorklet dan manfaatnya bagi web. Manfaat yang jelas adalah lebih banyak kontrol atas animasi dan cara baru untuk mendorong animasi untuk membawa tingkat ketelitian visual yang baru ke web. Namun, API juga memungkinkan Anda membuat aplikasi lebih tahan terhadap jank sambil akses terhadap semua kebaikan yang baru pada saat yang sama.
Worklet Animasi ada dalam Canary dan kami menargetkan Uji Coba Origin dengan Chrome 71. Kami sangat menantikan pengalaman web baru dan pengalaman hebat Anda tentang apa yang dapat kami tingkatkan. Tersedia juga polyfill yang memberi Anda API yang sama, tetapi tidak menyediakan isolasi performa.
Perlu diingat bahwa Transisi CSS dan Animasi CSS masih valid pilihan dan bisa jauh lebih sederhana untuk animasi dasar. Tetapi jika Anda ingin keren, AnimationWorklet siap membantu Anda!