Kompleksitas scroller tanpa batas

TL;DR: Gunakan kembali elemen DOM dan hapus elemen yang jauh dari area tampilan. Gunakan placeholder untuk memperhitungkan data yang tertunda. Berikut adalah demo dan kode untuk scroll tanpa batas.

Scrolling tanpa batas muncul di seluruh internet. Daftar artis Google Music adalah satu, linimasa Facebook adalah satu, dan feed live Twitter juga satu. Anda men-scroll ke bawah dan sebelum mencapai bagian bawah, konten baru akan muncul secara ajaib. Pengalaman ini lancar bagi pengguna dan mudah dilihat daya tariknya.

Namun, tantangan teknis di balik scroll tanpa batas lebih sulit daripada yang terlihat. Rentang masalah yang Anda temui saat ingin melakukan Hal yang Benar™ sangat luas. Hal ini dimulai dengan hal-hal sederhana seperti link di footer yang praktis tidak dapat dijangkau karena konten terus mendorong footer. Namun, masalahnya menjadi lebih sulit. Bagaimana cara menangani peristiwa pengubahan ukuran saat seseorang mengubah ponsel dari potret ke lanskap atau bagaimana cara mencegah ponsel berhenti secara tiba-tiba saat daftar menjadi terlalu panjang?

The right thing™

Kami pikir itu sudah cukup alasan untuk membuat implementasi referensi yang menunjukkan cara mengatasi semua masalah ini dengan cara yang dapat digunakan kembali sekaligus mempertahankan standar performa.

Kita akan menggunakan 3 teknik untuk mencapai sasaran: daur ulang DOM, tombstone, dan anchor scroll.

Kasus demo kita akan berupa jendela chat seperti Hangouts tempat kita dapat men-scroll pesan. Hal pertama yang kita perlukan adalah sumber pesan chat tanpa batas. Secara teknis, tidak ada scroll tanpa batas yang benar-benar tanpa batas, tetapi dengan jumlah data yang tersedia untuk dimasukkan ke dalam scroll ini, scroll tersebut mungkin juga tanpa batas. Untuk memudahkan, kita hanya akan melakukan hard code pada serangkaian pesan chat dan memilih pesan, penulis, dan lampiran gambar sesekali secara acak dengan sedikit penundaan buatan agar berperilaku sedikit lebih seperti jaringan sebenarnya.

Screenshot aplikasi Chat

Daur ulang DOM

Pemulihan DOM adalah teknik yang kurang dimanfaatkan untuk menjaga jumlah node DOM tetap rendah. Ide umumnya adalah menggunakan elemen DOM yang sudah dibuat dan berada di luar layar, bukan membuat elemen baru. Memang, node DOM itu sendiri murah, tetapi tidak gratis, karena setiap node menambahkan biaya tambahan dalam memori, tata letak, gaya, dan gambar. Perangkat kelas bawah akan menjadi jauh lebih lambat, jika tidak sepenuhnya tidak dapat digunakan, jika situs memiliki DOM yang terlalu besar untuk dikelola. Perlu diingat juga bahwa setiap tata letak ulang dan penerapan ulang gaya Anda – proses yang dipicu setiap kali class ditambahkan atau dihapus dari node – menjadi lebih mahal dengan DOM yang lebih besar. Dengan mendaur ulang node DOM, kami akan mempertahankan jumlah total node DOM secara signifikan lebih rendah, sehingga semua proses ini menjadi lebih cepat.

Hambatan pertama adalah scroll itu sendiri. Karena kita hanya akan memiliki sebagian kecil dari semua item yang tersedia di DOM pada waktu tertentu, kita perlu menemukan cara lain agar scrollbar browser mencerminkan jumlah konten yang secara teoritis ada dengan benar. Kita akan menggunakan elemen sentinel 1 piksel kali 1 piksel dengan transformasi untuk memaksa elemen yang berisi item – landasan pacu – agar memiliki tinggi yang diinginkan. Kita akan mempromosikan setiap elemen di landasan ke lapisannya sendiri untuk memastikan lapisan landasan itu sendiri benar-benar kosong. Tidak ada warna latar belakang, apa pun. Jika lapisan landasan tidak kosong, lapisan tersebut tidak memenuhi syarat untuk pengoptimalan browser dan kita harus menyimpan tekstur di kartu grafis yang memiliki tinggi beberapa ratus ribu piksel. Tidak dapat digunakan di perangkat seluler.

Setiap kali men-scroll, kita akan memeriksa apakah area pandang sudah cukup dekat dengan ujung landasan. Jika demikian, kita akan memperluas landasan dengan memindahkan elemen sentinel dan memindahkan item yang telah keluar dari area pandang ke bagian bawah landasan dan mengisinya dengan konten baru.

Runway Sentinel Viewport

Hal yang sama berlaku untuk men-scroll ke arah lain. Namun, kita tidak akan pernah menyingkat landasan pacu dalam penerapan kita, sehingga posisi scrollbar tetap konsisten.

Tombstone

Seperti yang telah disebutkan sebelumnya, kita mencoba membuat sumber data berperilaku seperti sesuatu di dunia nyata. Dengan latensi jaringan dan semuanya. Artinya, jika pengguna menggunakan scroll cepat, mereka dapat dengan mudah men-scroll melewati elemen terakhir yang datanya kita miliki. Jika hal itu terjadi, kami akan menempatkan item tombstone – placeholder – yang akan diganti oleh item dengan konten sebenarnya setelah data tiba. Tombstone juga didaur ulang dan memiliki kumpulan terpisah untuk elemen DOM yang dapat digunakan kembali. Kita memerlukannya agar dapat melakukan transisi yang baik dari tombstone ke item yang diisi dengan konten, yang akan sangat mengganggu pengguna dan mungkin benar-benar membuat mereka kehilangan fokus pada apa yang mereka fokuskan.

Makam
tersebut. Sangat batu. Wow.

Tantangan yang menarik di sini adalah item sebenarnya dapat memiliki tinggi yang lebih besar daripada item tombstone karena jumlah teks yang berbeda per item atau gambar terlampir. Untuk mengatasi hal ini, kita akan menyesuaikan posisi scroll saat ini setiap kali data masuk dan tombstone diganti di atas area pandang, dengan mengikat posisi scroll ke elemen, bukan nilai piksel. Konsep ini disebut anchor scroll.

Arah scroll

Penyematan scroll kami akan dipanggil saat tombstone diganti serta saat jendela diubah ukurannya (yang juga terjadi saat perangkat dibalik). Kita harus mencari tahu elemen yang paling terlihat di bagian atas area pandang. Karena elemen tersebut hanya dapat terlihat sebagian, kita juga akan menyimpan offset dari bagian atas elemen tempat area pandang dimulai.

Diagram anchor scroll.

Jika ukuran area pandang diubah dan landasan pacu mengalami perubahan, kita dapat memulihkan situasi yang secara visual terasa sama bagi pengguna. Menang! Kecuali jendela yang diubah ukurannya berarti setiap item berpotensi mengubah tingginya, jadi bagaimana kita mengetahui seberapa jauh konten yang ditautkan harus ditempatkan? Tidak. Untuk mengetahuinya, kita harus menata letak setiap elemen di atas item yang ditautkan dan menambahkan semua tingginya; hal ini dapat menyebabkan jeda yang signifikan setelah perubahan ukuran, dan kita tidak ingin hal itu. Sebagai gantinya, kita mengasumsikan bahwa setiap item di atas memiliki ukuran yang sama dengan tombstone dan menyesuaikan posisi scroll kita. Saat elemen di-scroll ke dalam landasan, kita menyesuaikan posisi scroll, yang secara efektif menunda pekerjaan tata letak hingga saat benar-benar diperlukan.

Tata Letak

Saya telah melewatkan detail penting: Tata letak. Setiap daur ulang elemen DOM biasanya akan menata ulang seluruh landasan yang akan membuat kita jauh di bawah target 60 frame per detik. Untuk menghindari hal ini, kita akan mengambil beban tata letak dan menggunakan elemen yang diposisikan secara mutlak dengan transformasi. Dengan cara ini, kita dapat berpura-pura bahwa semua elemen di bagian atas landasan masih menggunakan ruang, padahal sebenarnya hanya ada ruang kosong. Karena kita melakukan tata letak sendiri, kita dapat meng-cache posisi tempat setiap item berakhir dan kita dapat segera memuat elemen yang benar dari cache saat pengguna men-scroll mundur.

Idealnya, item hanya akan dicat ulang sekali saat dilampirkan ke DOM dan tidak terpengaruh oleh penambahan atau penghapusan item lain di runway. Hal ini mungkin, tetapi hanya dengan browser modern.

Penyesuaian terbaru

Baru-baru ini, Chrome menambahkan dukungan untuk Pembatasan CSS, fitur yang memungkinkan developer memberi tahu browser bahwa elemen adalah batas untuk pekerjaan tata letak dan gambar. Karena kita melakukan tata letak sendiri di sini, ini adalah aplikasi utama untuk pembatasan. Setiap kali menambahkan elemen ke landasan, kita tahu item lain tidak perlu terpengaruh oleh tata letak ulang. Jadi, setiap item harus mendapatkan contain: layout. Kita juga tidak ingin memengaruhi situs kita yang lain, jadi runway itu sendiri juga harus mendapatkan perintah gaya ini.

Hal lain yang kami pertimbangkan adalah menggunakan IntersectionObservers sebagai mekanisme untuk mendeteksi kapan pengguna telah men-scroll cukup jauh agar kita dapat mulai mendaur ulang elemen dan memuat data baru. Namun, IntersectionObservers ditentukan untuk memiliki latensi tinggi (seperti menggunakan requestIdleCallback), sehingga kita mungkin benar-benar merasa kurang responsif dengan IntersectionObservers daripada tanpanya. Bahkan implementasi saat ini yang menggunakan peristiwa scroll mengalami masalah ini, karena peristiwa scroll dikirim berdasarkan “upaya terbaik”. Pada akhirnya, Worklet Compositor Houdini akan menjadi solusi fidelitas tinggi untuk masalah ini.

Masih belum sempurna

Implementasi daur ulang DOM saat ini tidak ideal karena menambahkan semua elemen yang melewati area pandang, bukan hanya memperhatikan elemen yang sebenarnya ada di layar. Artinya, saat men-scroll sangat cepat, Anda memberikan begitu banyak pekerjaan untuk tata letak dan gambar di Chrome sehingga tidak dapat mengimbanginya. Anda akan hanya melihat latar belakang. Ini bukan akhir dari segalanya, tetapi tentunya ada hal yang perlu ditingkatkan.

Kami harap Anda melihat betapa menantangnya masalah sederhana saat Anda ingin menggabungkan pengalaman pengguna yang luar biasa dengan standar performa yang tinggi. Dengan Progressive Web Apps menjadi pengalaman inti di ponsel, hal ini akan menjadi lebih penting dan developer web harus terus berinvestasi dalam menggunakan pola yang menghormati batasan performa.

Semua kode dapat ditemukan di repositori kami. Kami telah melakukan upaya terbaik untuk membuatnya tetap dapat digunakan kembali, tetapi tidak akan memublikasikannya sebagai library sebenarnya di npm atau sebagai repo terpisah. Penggunaan utamanya adalah edukasi.