Pelajari cara membangun daftar dashboard cepat dengan 100k baris menggunakan pagination, virtualisasi, filter pintar, dan query yang lebih baik agar tool internal tetap responsif.

Sebuah layar daftar biasanya terasa baik sampai suatu saat tidak lagi. Pengguna mulai melihat jeda kecil yang menumpuk: scroll tersendat, halaman macet sebentar setelah setiap pembaruan, filter butuh detik untuk merespons, dan Anda melihat spinner setelah setiap klik. Kadang tab browser tampak beku karena thread UI sibuk.
100k baris adalah titik yang sering menonjol karena menekan setiap bagian sistem sekaligus. Dataset itu masih wajar untuk database, tapi cukup besar untuk membuat ketidakefisienan kecil menjadi jelas di browser dan di jaringan. Jika Anda berusaha menampilkan semuanya sekaligus, layar sederhana berubah menjadi pipeline berat.
Tujuannya bukan merender semua baris. Tujuannya membantu seseorang menemukan apa yang mereka butuhkan dengan cepat: 50 baris yang tepat, halaman berikutnya, atau irisan sempit berdasarkan filter.
Membagi pekerjaan menjadi empat bagian membantu:
Jika salah satu bagian mahal, keseluruhan layar terasa lambat. Kotak pencarian sederhana bisa memicu request yang mengurutkan 100k baris, mengembalikan ribuan record, lalu memaksa browser merender semuanya. Begitulah mengetik menjadi lag.
Saat tim membangun tool internal dengan cepat (termasuk menggunakan platform vibe-coding seperti Koder.ai), layar daftar sering kali tempat pertama di mana pertumbuhan data nyata memperlihatkan celah antara “bekerja pada dataset demo” dan “terasa instan setiap hari.”
Sebelum mengoptimalkan, putuskan apa arti cepat untuk layar ini. Banyak tim mengejar throughput (memuat semuanya) padahal pengguna sebenarnya butuh latensi rendah (melihat sesuatu berubah dengan cepat). Sebuah daftar bisa terasa instan meskipun tidak pernah memuat semua 100k baris, selama merespons cepat terhadap scroll, sort, dan filter.
Target praktis adalah waktu ke baris pertama, bukan waktu hingga terisi penuh. Pengguna percaya halaman ketika mereka melihat 20–50 baris pertama dengan cepat dan interaksi tetap mulus.
Pilih beberapa angka kecil yang bisa Anda lacak setiap kali mengubah sesuatu:
COUNT(*) dan SELECT yang lebar)Ini berkaitan langsung dengan gejala umum. Jika CPU browser melonjak saat Anda menggulir, frontend melakukan terlalu banyak kerja per baris. Jika spinner menunggu namun scroll baik-baik saja setelahnya, biasanya masalah ada di backend atau jaringan. Jika request cepat tapi halaman tetap membeku, hampir selalu masalah rendering atau pemrosesan berat di sisi klien.
Coba satu eksperimen sederhana: pertahankan UI sama, tapi batasi backend sementara agar hanya mengembalikan 20 baris dengan filter yang sama. Jika jadi cepat, bottleneck Anda adalah ukuran beban atau waktu query. Jika masih lambat, periksa rendering, formatting, dan komponen per-barus.
Contoh: layar Orders internal terasa lambat saat Anda mengetik di pencarian. Jika API mengembalikan 5.000 baris dan browser memfilter semuanya di setiap penekanan tombol, mengetik akan lag. Jika API butuh 2 detik karena COUNT pada filter tanpa index, Anda akan menunggu sebelum baris apa pun berubah. Perbaikan berbeda, keluhan pengguna sama.
Browser sering menjadi bottleneck pertama. Sebuah daftar bisa terasa lambat padahal API cepat, hanya karena halaman berusaha mewarnai terlalu banyak hal. Aturan pertama sederhana: jangan merender ribuan baris di DOM sekaligus.
Bahkan sebelum menambah virtualisasi penuh, buat setiap baris ringan. Baris dengan pembungkus bersarang, ikon, tooltip, dan style kondisional kompleks di setiap sel akan menguras saat scroll dan setiap pembaruan. Pilih teks polos, beberapa badge kecil, dan satu atau dua elemen interaktif per baris.
Tinggi baris yang stabil membantu lebih dari yang terlihat. Ketika setiap baris sama tinggi, browser bisa memprediksi layout dan scroll tetap mulus. Baris dengan tinggi variabel (deskripsi yang membungkus, catatan yang bisa diperluas, avatar besar) memicu pengukuran ekstra dan reflow. Jika Anda perlu detail tambahan, pertimbangkan panel samping atau area yang bisa diperluas sekali saja, bukan baris multiline penuh.
Formatting juga biaya yang diam-diam. Tanggal, mata uang, dan pekerjaan string berat bertambah ketika diulang di banyak sel.
Jika sebuah nilai tidak terlihat, jangan hitung dulu. Cache hasil formatting yang mahal dan hitung sesuai permintaan, misalnya ketika baris menjadi terlihat atau saat pengguna membuka baris.
Langkah cepat yang sering memberi peningkatan nyata:
Contoh: tabel Invoices internal yang memformat 12 kolom mata uang dan tanggal akan tersendat saat scroll. Mencache nilai terformat per invoice dan menunda pekerjaan untuk baris di luar layar bisa membuatnya terasa instan, bahkan sebelum kerja backend yang lebih dalam.
Virtualisasi berarti tabel hanya menggambar baris yang benar-benar terlihat (plus buffer kecil di atas dan bawah). Saat Anda menggulir, elemen DOM yang sama dipakai ulang dan data diganti di dalamnya. Itu menjaga browser dari mencoba mewarnai puluhan ribu komponen baris sekaligus.
Virtualisasi cocok saat Anda punya daftar panjang, tabel lebar, atau baris berat (avatar, chip status, menu aksi, tooltip). Juga berguna saat pengguna sering menggulir dan mengharapkan tampilan kontinu yang mulus daripada pindah halaman demi halaman.
Ini bukan sulap. Beberapa hal sering mengejutkan:
Pendekatan paling sederhana adalah membosankan: tinggi baris tetap, kolom yang dapat diprediksi, dan tidak terlalu banyak widget interaktif di tiap baris.
Anda bisa menggabungkan keduanya: gunakan pagination (atau load more berbasis cursor) untuk membatasi apa yang Anda ambil dari server, dan virtualisasi untuk menjaga rendering murah di dalam slice yang diambil.
Pola praktis adalah mengambil ukuran halaman normal (sering 100 sampai 500 baris), virtualisasi dalam halaman itu, dan tawarkan kontrol yang jelas untuk berpindah halaman. Jika menggunakan infinite scroll, tambahkan indikator Loaded X of Y yang terlihat agar pengguna mengerti mereka belum melihat semuanya.
Jika Anda butuh layar daftar yang tetap bisa digunakan saat data tumbuh, pagination biasanya default paling aman. Ia dapat diprediksi, bekerja baik untuk alur admin (review, edit, approve), dan mendukung kebutuhan umum seperti mengekspor "halaman 3 dengan filter ini" tanpa kejutan. Banyak tim akhirnya kembali ke pagination setelah mencoba scrolling yang lebih canggih.
Infinite scroll bisa terasa enak untuk browsing kasual, tapi punya biaya tersembunyi. Orang kehilangan orientasi, tombol kembali sering tidak mengembalikan ke posisi yang sama, dan sesi panjang bisa menumpuk memori saat lebih banyak baris dimuat. Jalan tengahnya adalah tombol Load more yang masih menggunakan halaman, sehingga pengguna tetap terorientasi.
Offset pagination adalah pendekatan klasik page=10&size=50. Sederhana, tapi bisa melambat pada tabel besar karena database harus melewati banyak baris untuk mencapai halaman yang lebih jauh. Juga bisa terasa aneh saat baris baru masuk dan item bergeser antar halaman.
Keyset pagination (sering disebut cursor pagination) meminta "50 baris berikutnya setelah item terakhir yang terlihat," biasanya menggunakan id atau created_at. Ini cenderung tetap cepat karena tidak perlu menghitung dan melewati begitu banyak kerja.
Aturan praktis:
Pengguna suka melihat total, tapi count all matching rows penuh bisa mahal dengan filter berat. Opsi termasuk cache count untuk filter populer, memperbarui count di background setelah halaman dimuat, atau menampilkan count perkiraan (mis. "10.000+").
Contoh: layar Orders internal bisa menampilkan hasil instan dengan keyset pagination, lalu mengisi total tepat hanya ketika pengguna berhenti mengubah filter selama satu detik.
Jika Anda membangun ini di Koder.ai, anggap perilaku pagination dan count sebagai bagian dari spesifikasi layar sejak awal, supaya query backend dan state UI yang digenerate tidak saling bertentangan nanti.
Kebanyakan layar daftar terasa lambat karena dimulai terlalu terbuka: memuat semuanya, lalu meminta pengguna mempersempit. Balik urutan itu. Mulai dengan default yang masuk akal yang mengembalikan set kecil dan berguna (misalnya: 7 Hari Terakhir, Item Saya, Status: Open), dan buat pilihan All time menjadi eksplisit.
Pencarian teks adalah jebakan umum lain. Jika Anda menjalankan query pada setiap penekanan tombol, Anda membuat antrean request dan UI yang berkedip. Debounce input pencarian sehingga Anda hanya men-query setelah pengguna berhenti sejenak, dan batalkan request lama saat request baru dimulai. Aturan sederhana: jika pengguna masih mengetik, jangan panggil server dulu.
Filter terasa cepat juga ketika jelas. Tampilkan chip filter di dekat atas tabel agar pengguna melihat apa yang aktif dan bisa menghapusnya dengan satu klik. Gunakan label chip yang manusiawi, bukan nama field mentah (mis. Owner: Sam daripada owner_id=42). Ketika seseorang bilang "hasil saya menghilang," biasanya ada filter yang tak terlihat.
Polanya yang menjaga daftar besar responsif tanpa membuat UI rumit:
Saved views adalah pahlawan tak terlihat. Daripada mengajari pengguna membangun kombinasi filter satu kali yang sempurna setiap kali, berikan beberapa preset yang cocok dengan alur kerja nyata. Tim ops mungkin berpindah antara Failed payments today dan High-value customers. Itu bisa satu klik, langsung dimengerti, dan lebih mudah dijaga cepat di backend.
Jika Anda membangun tool internal di builder yang digerakkan chat seperti Koder.ai, perlakukan filter sebagai bagian alur produk, bukan tambahan. Mulai dari pertanyaan paling umum, lalu desain view default dan saved views berdasarkan itu.
Layar daftar jarang membutuhkan data yang sama seperti halaman detail. Jika API Anda mengembalikan segala sesuatu tentang segala sesuatu, Anda membayar dua kali: database melakukan lebih banyak kerja, dan browser menerima serta merender lebih dari yang bisa digunakan. Query shaping adalah kebiasaan hanya meminta apa yang diperlukan daftar saat ini.
Mulai dengan mengembalikan hanya kolom yang dibutuhkan untuk merender tiap baris. Untuk kebanyakan dashboard, itu id, beberapa label, status, pemilik, dan timestamps. Teks besar, blob JSON, dan field terkomputasi bisa menunggu hingga pengguna membuka baris.
Hindari join berat untuk first paint. Join baik ketika mengenai index dan mengembalikan hasil kecil, tapi menjadi mahal ketika Anda join banyak tabel lalu mengurutkan atau memfilter berdasarkan data join. Pola sederhana: ambil daftar dari satu tabel dengan cepat, lalu muat detail terkait sesuai permintaan (atau batch-load untuk baris yang terlihat saja).
Batasi opsi pengurutan dan urutkan berdasarkan kolom yang diindeks. "Sort by anything" terdengar membantu, tapi sering memaksa sort lambat pada dataset besar. Pilih beberapa opsi yang dapat diprediksi seperti created_at, updated_at, atau status, dan pastikan kolom-kolom itu diindeks.
Hati-hati dengan agregasi sisi-server. COUNT(*) pada set yang difilter besar, DISTINCT pada kolom lebar, atau perhitungan total halaman bisa mendominasi waktu respons.
Pendekatan praktis:
COUNT dan DISTINCT sebagai opsional; cache atau perkirakan bila mungkinJika Anda membangun tool internal di Koder.ai, definisikan query daftar ringan terpisah dari query detail saat perencanaan, supaya UI tetap snappy saat data tumbuh.
Jika Anda ingin layar daftar tetap cepat pada 100k baris, database harus melakukan lebih sedikit kerja per permintaan. Kebanyakan daftar lambat bukan karena "terlalu banyak data," melainkan pola akses data yang salah.
Mulai dengan index yang cocok dengan apa yang pengguna lakukan sebenarnya. Jika daftar biasanya difilter oleh status dan diurutkan oleh created_at, Anda ingin index yang mendukung kedua hal itu, dalam urutan tersebut. Kalau tidak, database bisa memindai jauh lebih banyak baris dari yang diharapkan lalu mengurutkannya, yang cepat menjadi mahal.
Perbaikan yang biasanya memberi kemenangan terbesar:
tenant_id, status, created_at).OFFSET yang dalam. OFFSET membuat database melangkahi banyak baris hanya untuk melewatinya.Contoh sederhana: tabel Orders internal yang menampilkan nama pelanggan, status, jumlah, dan tanggal. Jangan join setiap tabel terkait dan tarik full order notes untuk view daftar. Kembalikan hanya kolom yang dipakai di tabel, dan muat sisanya di request terpisah saat pengguna mengklik order.
Jika Anda membangun dengan platform seperti Koder.ai, pertahankan pola pikir ini meskipun UI digenerate dari chat. Pastikan endpoint API yang dihasilkan mendukung cursor pagination dan field yang selektif, sehingga kerja database tetap dapat diprediksi saat tabel tumbuh.
Jika halaman daftar terasa lambat hari ini, jangan mulai dengan menulis ulang semuanya. Mulai dengan mengunci bagaimana penggunaan normal terlihat, lalu optimalkan jalur itu.
Tentukan view default. Pilih filter default, urutan, dan kolom yang terlihat. Daftar melambat bila mencoba menampilkan segalanya secara default.
Pilih gaya paging yang sesuai penggunaan. Jika pengguna biasanya melihat beberapa halaman pertama, pagination klasik sudah cukup. Jika orang melompat jauh (halaman 200+) atau Anda butuh performa stabil tak peduli sejauh apa mereka pergi, gunakan keyset pagination (berdasarkan sort stabil seperti created_at plus id).
Tambahkan virtualisasi untuk body tabel. Bahkan jika backend cepat, browser bisa kewalahan saat merender terlalu banyak baris sekaligus.
Buat pencarian dan filter terasa instan. Debounce mengetik agar Anda tidak mengirim request pada setiap penekanan tombol. Simpan state filter di URL atau store bersama sehingga refresh, tombol kembali, dan berbagi view bekerja andal. Cache hasil sukses terakhir supaya tabel tidak berkedip kosong.
Ukur, lalu tuning query dan index. Log waktu server, waktu database, ukuran payload, dan waktu render. Lalu pangkas query: select hanya kolom yang Anda tunjukkan, terapkan filter lebih awal, dan tambahkan index yang cocok dengan filter + sort default Anda.
Contoh: dashboard support internal dengan 100k tiket. Default ke Open, ditugaskan ke tim saya, urutkan berdasarkan terbaru, tampilkan enam kolom, dan hanya ambil ticket id, subject, assignee, status, dan timestamps. Dengan keyset pagination dan virtualisasi, Anda menjaga database dan UI tetap dapat diprediksi.
Jika Anda membangun tool internal di Koder.ai, rencana ini cocok untuk workflow iterasi-dan-cek: sesuaikan view, uji scroll dan pencarian, lalu tuning query sampai halaman tetap snappy.
Cara tercepat membuat layar daftar terasa rusak adalah memperlakukan 100k baris seperti halaman data biasa. Sebagian besar dashboard lambat memiliki beberapa jebakan yang bisa diprediksi.
Satu yang besar adalah merender semuanya lalu menyembunyikannya dengan CSS. Meski terlihat hanya 50 baris yang terlihat, browser tetap membayar untuk membuat 100k node DOM, mengukurnya, dan me-repaint saat scroll. Jika butuh daftar panjang, render hanya apa yang pengguna bisa lihat (virtualisasi) dan buat komponen baris sederhana.
Pencarian juga bisa merusak performa secara diam-diam ketika setiap penekanan tombol memicu full table scan. Itu terjadi saat filter tidak didukung oleh index, saat Anda mencari di terlalu banyak kolom, atau saat Anda menjalankan query contains di field teks besar tanpa rencana. Aturan bagus: filter pertama yang sering dipakai pengguna harus murah di database, bukan hanya nyaman di UI.
Masalah umum lain adalah mengambil record penuh padahal daftar hanya butuh ringkasan. Sebuah baris daftar biasanya butuh 5–12 field, bukan seluruh objek, bukan deskripsi panjang, dan bukan data terkait. Menarik data ekstra meningkatkan kerja database, waktu jaringan, dan parsing frontend.
Export dan total bisa membekukan UI jika Anda menghitungnya di main thread atau menunggu request berat sebelum merespons. Jaga UI tetap interaktif: mulai ekspor di background, tampilkan progres, dan hindari menghitung ulang total pada setiap perubahan filter.
Terakhir, terlalu banyak opsi sort bisa berbalik merugikan. Jika pengguna bisa mengurutkan menurut kolom apa pun, Anda akan mengurutkan set besar di memori atau memaksa database ke rencana lambat. Batasi sort ke beberapa kolom yang diindeks, dan buat sort default cocok dengan index nyata.
Pengecekan cepat insting:
Perlakukan performa daftar sebagai fitur produk, bukan perbaikan sekali jadi. Sebuah layar daftar cepat hanya ketika terasa cepat saat orang nyata menggulir, memfilter, dan mengurut pada data nyata.
Gunakan daftar periksa ini untuk memastikan Anda memperbaiki hal yang tepat:
Pemeriksaan realitas sederhana: buka daftar, gulir selama 10 detik, lalu terapkan filter umum (mis. Status: Open). Jika UI membeku, masalah biasanya rendering (terlalu banyak DOM rows) atau transformasi sisi-klien berat (sorting, grouping, formatting) yang terjadi pada setiap pembaruan.
Langkah berikutnya, berurutan, supaya Anda tidak bolak-balik memperbaiki:
Jika Anda membangun ini dengan Koder.ai (koder.ai), mulai di Planning Mode: tentukan kolom daftar, field filter, dan bentuk respons API terlebih dahulu. Lalu iterasi menggunakan snapshot dan rollback saat eksperimen memperlambat layar.
Ubah tujuan dari “memuat semuanya” menjadi “menunjukkan baris pertama yang berguna dengan cepat.” Optimalkan waktu hingga baris pertama dan interaksi yang mulus saat memfilter, mengurutkan, dan menggulir, bahkan jika seluruh dataset tidak pernah dimuat sekaligus.
Ukur waktu sampai baris pertama muncul setelah memuat atau mengganti filter, waktu untuk filter/urut menampilkan hasil baru, ukuran payload respons, query database yang lambat (terutama COUNT(*) dan SELECT lebar), serta lonjakan di main-thread browser. Angka-angka itu langsung mencerminkan apa yang pengguna rasakan sebagai “lag.”
Batasi sementara API untuk hanya mengembalikan 20 baris dengan filter dan pengurutan yang sama. Jika jadi cepat, Anda sebagian besar membayar biaya query atau ukuran payload; jika tetap lambat, biasanya masalah ada pada rendering, formatting, atau kerja sisi-klien per baris.
Jangan render ribuan baris di DOM sekaligus, buat komponen baris sederhana, dan pertahankan tinggi baris tetap. Juga hindari mengerjakan formatting berat untuk baris yang berada di luar layar; hitung dan cache formatting hanya saat baris terlihat atau dibuka.
Virtualisasi hanya memasang baris yang terlihat (plus buffer kecil), menggunakan kembali elemen DOM saat menggulir. Ini efektif bila pengguna banyak menggulir atau baris “berat”, tapi bekerja paling baik ketika tinggi baris konsisten dan tata letak tabel dapat diprediksi.
Pagination adalah pilihan default yang paling aman untuk kebanyakan alur admin dan internal karena membantu orientasi pengguna dan membatasi kerja server. Infinite scroll cocok untuk browsing santai, tapi sering membuat navigasi dan penggunaan memori menjadi lebih buruk kecuali Anda mengelolanya dengan jelas.
Offset pagination lebih sederhana tetapi bisa melambat pada halaman yang jauh karena database harus melewati banyak baris. Keyset (cursor) pagination biasanya tetap cepat karena melanjutkan dari rekaman terakhir, namun kurang cocok untuk langsung lompat ke nomor halaman tertentu.
Jangan menjalankan request pada setiap penekanan tombol. Debounce input, batalkan request yang sedang berjalan saat request baru dimulai, dan defaultkan ke filter yang mempersempit (mis. rentang tanggal pendek atau “item saya”) sehingga query pertama kecil dan berguna.
Kembalikan hanya field yang benar-benar dirender daftar, biasanya beberapa field seperti id, label, status, pemilik, dan timestamps. Pindahkan teks besar, JSON blob, dan data terkait lainnya ke request detail agar first paint tetap ringan dan dapat diprediksi.
Sesuaikan default filter dan pengurutan dengan penggunaan nyata, lalu tambahkan index yang mendukung pola itu (sering kali index komposit yang menggabungkan tenant/field filter dengan kolom pengurutan). Anggap total exact sebagai opsional: tampilkan nanti, cache, atau perkiraan agar tidak memblokir respons utama.