- Saat membuat GUI basis data untuk MongoDB dan PostgreSQL, perlu mendukung tipe BSON/JSONB, kolom bersarang, pencarian, pengeditan, pinning, dan drag; untuk itu, selama sekitar satu tahun struktur virtualisasi dua sumbu dan manajemen status dioptimalkan
- Dibuat shadow table yang, terpisah dari dokumen asli, menghitung lebih dulu string tampilan, tipe, jalur yang diratakan, urutan kolom, dan hasil pencarian; hanya baris dan kolom yang terlihat di layar dirender sebagai DOM berukuran tetap
- Pada jalur scroll diterapkan passive event listener,
requestAnimationFrame, buffer dan hysteresis, serta pelacakan kecepatan; pekerjaan main thread dikurangi dengan memanfaatkantransformdanopacityalih-alih properti layout - Ikon per sel diganti menjadi gambar latar SVG bersama, editor hanya di-mount saat diperlukan, dan dengan DOM pooling yang melacak baris dan kolom berdasarkan posisi, pembuatan node saat scroll dihilangkan
- Canvas menawarkan batas atas performa 60fps yang lebih tinggi daripada DOM, tetapi kurang menguntungkan untuk teks, seleksi, aksesibilitas, dan perluasan fitur; karena itu dipilih desain berbasis DOM yang mempertahankan seleksi teks nyata dan pengembangan cepat
Tujuan dan batasan awal
- Awalnya dimulai dari array dua dimensi sederhana dan loop bersarang, tetapi berlanjut menjadi pekerjaan optimasi sporadis selama sekitar satu tahun
- Tabel pada GUI basis data bukan sekadar untuk ditampilkan, melainkan harus mendukung berbagai status dan interaksi
- Memahami semua tipe BSON MongoDB dan JSONB dari PostgreSQL dan lainnya, serta menampilkan ikon warna per tipe
- Membedakan tipe yang menghasilkan hasil kueri berbeda, seperti string
"123"dan integer123 - Memperluas dokumen bersarang menjadi subkolom nyata, mencari seluruh jalur bersarang, dan menyorot bagian yang cocok di dalam sel
- Diperlukan fitur pengurutan ulang kolom, pengubahan ukuran, pinning, pengeditan di dalam sel, serta drag nilai/baris/kolom ke visual query builder
- Status untuk fitur-fitur ini tidak ada pada dokumen asli dan harus tetap bertahan setelah scroll, sehingga diperlukan struktur rendering terpisah
Tahap 1: Merender semua item secara langsung
- Cara membuat semua sel dengan menelusuri baris dan field secara bersarang bekerja untuk 100 baris, tetapi runtuh pada data berskala besar
- 10.000 baris × 30 kolom menghasilkan sekitar 300.000 node DOM, dan change detection framework menelusurinya berulang kali
- Jika satu node DOM dianggap mengonsumsi sekitar 1KB hingga struktur internal browser, ratusan MB sudah dibutuhkan sebelum data sebenarnya
- Anggaran frame untuk 60fps adalah 16,7ms, dan style, layout, serta paint juga berbagi waktu ini
- Pada implementasi nyata, upaya merender 1.000 baris dengan sekitar 20 kolom gagal
- Untuk merender hanya sebagian, baris dan kolom yang sedang terlihat, urutan, ekspansi field bersarang, dan hasil pencarian harus dilacak secara terpisah
Tahap 2: Memisahkan status tampilan dengan shadow table
- Dokumen asli bersarang dan tipenya beragam, sehingga sulit dipakai sebagai input rendering
- MongoDB memiliki nilai BSON seperti ObjectId, Decimal128, timestamp, dan binary
- Data SQL memiliki JSONB dan timestamp dengan zona waktu, sehingga format perlu ditentukan untuk setiap sel
- Jika format ditentukan di loop rendering, biaya yang sama berulang setiap frame; data asli juga tidak memiliki status tabel seperti urutan kolom, status ekspansi, dan hasil pencarian
- Shadow table berfungsi sebagai titik acuan status yang akan ditampilkan tabel sebenarnya tanpa mengubah dokumen asli
- Dibangun sekali saat load dan diperbarui saat status berubah, tetapi tidak diubah saat scroll
- Untuk setiap sel, string tampilan yang dipotong, tipe yang sudah ditetapkan, dan jalur yang diratakan dihitung lebih dulu
- String tampilan dibatasi agar dokumen 16MB tidak menghasilkan string 16MB apa adanya dalam status rendering
- Tipe menentukan ikon, editor, dan cara pencarian
- Jalur rata seperti
"address.geo.lat"dipakai sebagai key agar tidak perlu menelusuri tree setiap kali
- Saat objek bersarang diperluas, subjalur dinaikkan menjadi kolom nyata; hasil pengurutan, pencarian, urutan kolom, dan status ekspansi juga disimpan dalam struktur yang sama
- Pada tahap ini jumlah DOM belum berkurang, tetapi fondasi untuk menghitung urutan dan lebar kolom dengan cepat sudah tersedia
Tahap 3: Virtualisasi vertikal dan area scroll hantu
- Virtualisasi vertikal hanya merender baris viewport dan sedikit buffer, sementara sisa tingginya dibuat sebagai area palsu
- Area hantu (phantom) adalah kontainer internal setinggi
rowCount × rowHeight- Jika 1 juta baris × 40px, dibuat
divsetinggi 40 juta px dengan nyaris tanpa isi - Browser menyediakan scrollbar native dan perilaku scroll berdasarkan tinggi ini
- Jika 1 juta baris × 40px, dibuat
- Rentang tampilan diperoleh dengan perhitungan berikut
firstRow = floor(scrollTop / rowHeight)lastRow = floor((scrollTop + viewportHeight) / rowHeight)- Baris buffer ditambahkan di kedua sisi agar tidak merender ulang pada setiap perpindahan kecil
- Baris yang ditampilkan ditempatkan pada kontainer slab di posisi
firstRow × rowHeight; menggunakantransformlebih menguntungkan daripadatop - Pengguna melihat 1 juta baris, tetapi DOM hanya memiliki sekitar 40 baris
- Namun jika ada 300 kolom, 40 baris saja sudah menghasilkan 12.000 sel, sehingga kolom juga harus divirtualisasi
Tahap 4: Virtualisasi horizontal untuk menangani lebar variabel
- Koleksi pada database dokumen dapat memiliki ratusan field, sehingga virtualisasi kolom juga diperlukan
- Karena lebar kolom tidak seragam, digunakan prefix sum dan binary search alih-alih pembagian dengan nilai tetap
- Dengan prefix sum berbentuk
position[n] = width[0] + ... + width[n-1], koordinat x tiap kolom diperoleh lewat satu lookup array - Kolom yang sesuai dengan offset scroll
xditemukan dengan binary search pada array prefix sum - Meski ada 1.000 kolom, pencarian selesai dalam skala mikrodetik
- Dengan prefix sum berbentuk
- Prefix sum dibangun ulang hanya saat lebar benar-benar berubah, seperti pengubahan ukuran kolom, penyembunyian, atau pengurutan ulang; tidak dibuat saat scroll
- Diberi buffer sekitar 200px di kiri dan kanan agar kolom berikutnya dirender sebelum muncul di layar
- Setelah virtualisasi dua sumbu, area render tetap sekitar 40 baris × 12 kolom terlepas dari ukuran data
- Bahkan pada koleksi 500 kolom, hanya sekitar 12 kolom yang ada sekaligus, sehingga waktu load tidak bertambah
- Target rendering memang berkurang, tetapi masih ada masalah menghitung ulang rentang pada setiap event scroll yang terjadi ratusan kali per detik
Tahap 5: Mengelola anggaran frame pada jalur scroll
- Penanganan scroll berbagi anggaran frame 16,7ms dengan style, layout, dan paint, sehingga hanya pekerjaan minimum yang boleh dilakukan
- Passive event listener didaftarkan di luar change detection framework
- Memberi tahu browser bahwa
preventDefaulttidak akan dipanggil, sehingga compositor dapat memindahkan piksel tanpa menunggu JavaScript - Event scroll itu sendiri tidak memicu pemeriksaan rendering framework
- Memberi tahu browser bahwa
- Banyak event digabung menjadi satu kali per frame
- Hanya posisi scroll terbaru yang dicatat dan satu callback
requestAnimationFramedijadwalkan - Bahkan 12 event dalam satu frame diproses dengan satu perhitungan rentang
- Hanya posisi scroll terbaru yang dicatat dan satu callback
- Diterapkan early exit untuk fast draw yang dipelajari dari source Handsontable
- Jika rentang tampilan baru berada di dalam buffer render yang ada, cukup dua perbandingan integer lalu langsung return
- Hysteresis diterapkan pada batas buffer
- Saat rentang tampilan mendekati sekitar 40px dari tepi buffer, dilakukan pembangunan ulang
- Buffer sekitar 200px ditempatkan ulang berpusat pada posisi baru untuk mencegah tepi kosong terlihat atau pembangunan ulang berulang di batas
- Ditambahkan pendeteksi kecepatan yang melacak px/ms antar-event
- Dalam implementasinya, jika melebihi 10px/ms, itu dianggap flick cepat dan rendering dihentikan
- Native scroll dibiarkan bergerak di atas area hantu, lalu slab diisi kembali ketika kecepatan stabil
- Setelah pekerjaan JavaScript dikurangi pun penurunan frame masih tersisa; layout dan paint, termasuk style, striping, dan ikon, menjadi bottleneck berikutnya
Tahap 6: Menghilangkan biaya properti layout
- Main thread browser menangani style, layout, dan paint, sementara compositor memindahkan layer yang sudah digambar di GPU
- Properti yang bisa dianimasikan oleh compositor adalah
transformdanopacity, sedangkantop,left,width,height,background-color, dan sejenisnya membangunkan main thread - Saat
topdiperbarui untuk sinkronisasi scroll panel nomor baris dan kolom yang dipin, forced layout terjadi 60 kali per detik- Ini diganti dengan
translate3d, mempertahankan tampilan yang sama sambil menghilangkan biaya main thread
- Ini diganti dengan
- Striping yang sebelumnya diterapkan sebagai latar per baris diubah menjadi satu
repeating-linear-gradientuntuk seluruh body- Tinggi baris diteruskan sebagai variabel CSS
- Browser hanya meraster satu tile berukuran dua baris dan menyalinnya berulang dari tekstur GPU
- Binding class per baris hilang, dan body serta panel yang dipin berbagi tile yang sama sehingga mencegah ketidaksesuaian warna
- Garis pemisah kolom digambar hanya setinggi slab yang sedang dirender, bukan pada seluruh area hantu setinggi 40 juta px
- Scroll normal menjadi mulus, tetapi pada saat perpindahan window yang membuat sel baru, DOM yang tidak perlu per sel masih menumpuk dan menyisakan biaya
Tahap 7: Meringankan ikon dan editor
- Ikon tipe pada grid basis data bukan sekadar dekorasi, melainkan fitur untuk membedakan makna nilai seperti ObjectId, string, integer, dan JSONB
- Implementasi pertama menambahkan elemen ikon font pada setiap sel, tetapi node DOM tambahan dan jalur rendering teks untuk glyph menimbulkan biaya besar
- Ikon dipindahkan ke
background-imagesel itu sendiri dan dienkode sebagai SVG data URI- Semua sel bertipe sama merujuk string URI yang sama
- Browser meraster ikon per tipe hanya sekali dan menyalinnya berulang dari tekstur GPU yang di-cache
- Dekorasi berulang dapat ditampilkan tanpa node DOM terpisah
- Untuk pengeditan sel, digunakan pendekatan rendering campuran
- Pada kondisi normal, sel hanya menggunakan teks biasa dan satu
span - Hanya saat double-click, komponen editor berat yang sadar tipe di-mount seperti portal di atas sel tersebut
- Pada kondisi normal, sel hanya menggunakan teks biasa dan satu
- Jika semua sel memakai komponen framework, biaya pembuatan instance menumpuk; performa bisa turun ketika library grid digabungkan dengan komponen cell renderer
- Sel itu sendiri menjadi ringan, tetapi saat satu kolom baru muncul, masih ada masalah framework membuat ulang sel-sel lain yang isinya sama
Tahap 8: Daur ulang DOM berbasis posisi
- Kriteria pelacakan seperti
trackBydi Angular,keydi React dan Vue menentukan apakah elemen lama dipakai ulang atau dihancurkan lalu dibuat saat window bergerak - Jika baris tidak di-bind ulang ke data baru dan selalu diganti, komponen, node DOM, dan event listener harus terus dibuat ulang
- Baris dan kolom dilacak bukan berdasarkan nilai data, melainkan posisi di layar
- Sekitar 40 elemen baris dan kolom yang sama dipertahankan, dan hanya isinya diganti dengan nilai baru
- Grid berperilaku seperti object pool dan tidak mengalokasikan DOM baru saat scroll
- Pada tahap ini biaya scroll dua sumbu dan pembangunan ulang window teratasi, tetapi masih ada masalah finishing seperti goyangan satu frame saat drop kolom atau selisih setengah piksel pada nomor baris
Tahap 9: Menyelesaikan detail interaksi
- Saat kolom di-drag, kolom yang ada dipindahkan dengan
transformseolah-olah berada pada urutan baru; saat drop, pengurutan ulang dan resettransformdiproses dalam render pass yang sama- Frame drag terakhir dan frame pengurutan ulang pertama dibuat sama persis hingga level piksel agar transisi tidak terlihat
- Tooltip tidak di-bind pada setiap sel, melainkan ditangani dengan satu hover listener terdelegasi di kontainer
- Tooltip dihitung hanya untuk sel di bawah kursor
- Pembuatan string mahal, seperti pretty-print nilai JSONB SQL, juga ditunda hingga benar-benar hover
- Border 1px pada kolom nomor baris membuat baseline teks dan baris data bergeser setengah piksel, menimbulkan goyangan saat scroll
- Diperbaiki agar kedua kolom menggunakan box model yang sama
- Striping ditentukan berdasarkan indeks baris absolut, bukan posisi DOM yang dipakai ulang, sehingga warna tidak berkedip saat window virtualisasi berubah
- Fitur yang membenarkan pembangunan grid sendiri juga dipertahankan
- Dokumen bersarang tidak dibiarkan sebagai string JSON, melainkan diperluas menjadi subkolom nyata
- String yang cocok pada jalur bersarang disorot di dalam sel
- Nilai bertipe di-drag langsung dari grid ke visual query builder
- Biaya menambahkan fitur-fitur ini ke library grid umum lebih besar daripada biaya memiliki renderer sendiri
Tahap 10: Membaca source grid berperforma tinggi yang sudah ada
- Dalam belajar performa frontend, membaca source code proyek lain adalah kebiasaan paling bernilai, dan optimasi yang tidak terdokumentasi tersisa di repositori publik
- AG-Grid, grid DOM yang diteliti, menerapkan teknik berikut
- Time slicing untuk pekerjaan DOM dengan anggaran waktu per frame yang eksplisit dan antrean pekerjaan berprioritas
- Meng-hash viewport kolom sehingga scroll tanpa perubahan selesai hanya dengan satu perbandingan string
- Membuat baris mengikuti arah scroll agar konten di sisi yang dituju pengguna muncul terlebih dahulu
- Membuat sel baru terlebih dahulu dan menunda penghancuran sel lama, sehingga konten baru digambar sebelum konten yang menghilang
- Karena event listener per sel adalah bottleneck yang terukur, digunakan event delegation di tingkat kontainer
- Grid berbasis Canvas melewati DOM dan menggambar ulang ratusan teks setiap frame tanpa layout maupun style recalculation
- Dapat mempertahankan 60fps bahkan pada manipulasi berlebihan, sehingga batas atas performanya lebih tinggi daripada grid DOM
- Teks yang diraster di luar grid piksel bisa terlihat buram
- Seleksi hanya bekerja per sel, dan ellipsis tidak muncul jika tidak digambar sendiri
- Setiap fitur sel baru perlu menambahkan kode gambar dan kode hit-testing
- Pada akhirnya DOM dipilih untuk mempertahankan teks tajam, seleksi teks nyata, aksesibilitas, dan pengembangan fitur yang cepat
- Meski tidak mencapai kehalusan setingkat Canvas, ini adalah trade-off eksplisit yang mempertimbangkan pengalaman pengguna dan biaya pengembangan
Belum ada komentar.