![Numeric Pagination Itu Busuk!](https://images.ctfassets.net/7wjlcr9d5ii8/1BPcUpYX25lchuevC7MUjx/a68ef71cece34a003e6955d6dc188244/43f9f545b01a9642e0bcf3b9b4bcc415.png?w=1600&h=1200&q=100&fm=png&fit=thumb&f=faces)
Pagination adalah salah satu cara membagi record yang akan kita tampilkan dalam jumlah tertentu ke dalam beberapa halaman agar proses pemuatan record lebih cepat daripada memuat keseluruhan record dalam satu halaman. Karena tentu saja akan sangat lambat kalau kita memiliki 1juta record lalu semua record tersebut kita tampilkan pada satu halaman sekaligus. Apalagi kalau device user kentang banget speknya, bisa error not responding atau forced close dong. Untuk itu kita perlu membagi record tersebut menjadi beberapa halaman.
Kita perlu tahu dulu cara kerja pagination tersebut. Dari frontend akan memberikan parameter untuk menentukan itu halaman ke berapa, kita sebut saja nama parameternya disini sebagai pageNo
. Dan satu parameter lain untuk menentukan limitasi record pada satu halaman, kita sebut saja nama parameternya pageSize
. Misalkan pageNo=1 & pageSize=10, itu artinya kita akan menampilkan 10 record dari halaman pertama. Untuk melakukan limitasi, kita bisa menggunakan keyword limit
pada sql. Limit ini diisi sesuai pageSize
pada contoh parameter. Untuk melakukan paging, kita bisa menggunakan keyword offset
pada sql. Rumus Offset = ((pageNo – 1) * pageSize). Jadi misalkan mau mengambil halaman pertama berarti Offset = ((1 - 1) * 10) = 0, untuk mengambil halaman kedua berarti Offset = ((2 – 1) * 10) = 10. Query yang akan dieksekusi kurang lebih sebagai berikut:
-- pageNo=1&pageSize=10
SELECT o.order_no, o.note
FROM orders o
ORDER BY o.order_no
LIMIT 10 OFFSET 0
;
-- pageNo=2&pageSize=10
SELECT o.order_no, o.note
FROM orders o
ORDER BY o.order_no
LIMIT 10 OFFSET 10
;
-- pageNo=7&pageSize=10
SELECT o.order_no, o.note
FROM orders o
ORDER BY o.order_no
LIMIT 10 OFFSET 60
;
Nanti dari backend minimal ada 2 query yang dieksekusi, yaitu query mendapatkan list 10 record yang akan ditampilkan dan juga query untuk mendapatkan value total halaman atau total record yang ada. Jadi untuk melakukan itu, kita hitung dulu total recordnya seperti berikut:
SELECT count(*)
FROM orders o
;
Setelah itu kita hitung total page-nya. Rumusnya adalah Total Page = ((Total Record / Limit) + 1) yang dibulatkan tanpa koma. Limit adalah pageSize
seperti parameter di atas atau batas record yang akan kita tampilkan untuk satu halaman. Jadi misalkan kita ingin menampilkan 10 record untuk satu halaman, dan total record yang ada adalah 36, maka Total Page = ((36 / 10) + 1) = 4. Jadi dari backend selain menampilkan list 10 record, juga mengembalikan value total page, yaitu 4. Nanti dari frontend akan menampilkan urutan halaman sebanyak value total page dari backend yang ketika diklik akan menampilkan halaman yang diinginkan seperti gambar di atas. Itulah yang disebut Numeric Pagination.
Ketika total recordnya ga banyak, sekitar kurang dari 100ribu record mungkin ga akan terasa. Semuanya terlihat baik-baik saja. Tetapi semuanya akan berubah ketika recordnya mencapai jutaan. Semakin tinggi Offset, maka semakin tinggi pula query cost yang dihasilkan. Karena sql akan mengurutkan data dari halaman pertama hingga halaman ke-n yang dipilih. Misalkan user memilih halaman terakhir atau ke-sejuta, maka itu bisa berdampak pada penurunan performa query😱. Melakukan select count(*)
untuk jutaan data juga menguras kinerja database. Query-nya akan sangat lambat. Selain itu, Numeric Pagination kayak gini cenderung useless alias tidak berguna. User macam apa yang mau membuka masing-masing halaman yang berjumlah jutaan satu-persatu dari halaman satu sampai akhir? Kurang kerjaan banget usernya kalau ada😅. Ujung-ujungnya pasti apply filter lainnya juga. Lalu apa alternatifnya?
Ini adalah cara paling umum yang dilakukan oleh perbankan. Contohnya pada menu mutasi rekening internet banking atau mobile banking. Kita di-approach by default untuk memilih tanggal range tertentu sebelum menampilkan data. Selain itu, range-nya juga ga boleh terlalu jauh. Ada bank yang membatasi maksimal 3 bulan, 1 bulan, 31 hari, maupun 7 hari tergantung kebijakan masing-masing bank. Kita ga bisa melihat semua mutasi sekaligus. Bayangkan aja, kalau ada user yang menabung dari tahun 2000, rutin bertransaksi, trus dia memilih range dari tahun 2000 sampai tahun 2022. Bisa down server bank tersebut🤭. Pagination di sini tetap ada, tapi sekarang sudah dibatasi berdasarkan range waktu terpilih dan user “dipaksa” harus memilih range dalam waktu tertentu. Kalau user butuh data-data lama tinggal ganti date range pada filter. Ini bisa jadi salah satu alternatif yang digunakan dengan asumsi rata-rata record perbulan per-user ga nyampe ratusan ribu transaksi. Selain itu tanggal pada database sebaiknya diindex untuk optimasi filter date range.
-- pageNo=3&pageSize=10 from 01 january 2022 to 31 january 2022
SELECT o.order_no, o.note
FROM orders o
where o.ordering_date BETWEEN 20220101 and 20220131
LIMIT 10 OFFSET 20
;
-- total record from 01 january 2022 to 31 january 2022
SELECT count(*)
FROM orders o
where o.ordering_date BETWEEN 20220101 and 20220131
;
Cara kedua adalah menggunakan Lazy Load with Infinite Scroll. Pendekatan ini paling sering ditemui pada aplikasi modern. Kita ambil contohnya Grab Food. Ketika kita melakukan search makanan, kita akan ditampilkan 10 resto pertama. Lalu ketika kita scroll sampai bawah, nanti akan langsung loading fetch halaman kedua. Begitu seterusnya setiap scroll sampai bawah hingga pada halaman terakhir sudah tidak ada data lagi. Ini lebih baik karena dengan ini minimal kita hanya sekali query. Query yang dieksekusi hanya untuk mendapatkan 10 record pada halaman tersebut. Tidak perlu menghitung total record sama sekali seperti sebelumnya. User juga ga bakal betah scroll terus-terusan hingga halaman terakhir jika recordnya banyak, jadi eksekusi query halaman terakhir yang Offsetnya gede bisa dihindari dari segi UI/UX. Effortnya ada pada frontend yang perlu develop event onscroll
untuk fetch data setiap user scroll halaman sampai bawah.
window.onscroll = function() { myScrollFunction() };
function myScrollFunction() {
const innerHeight = window.innerHeight + document.documentElement.scrollTop;
const clientHeight = document.body.clientHeight;
const percentage = innerHeight / clientHeight * 100;
if (percentage >= 90) { //load when user scroll to 90% of page
fetch("https://your.api/here")
.then(o => o.json())
.then(o => {
//do yor things here...
})
}
}
Kekurangannya adalah user jadi kesulitan melihat footer jika ada. Karena akan kena fetch tiap kita scroll sampai bawah. Lalu, jika menggunakan SEO seperti untuk halaman utama sebuah website, ini kurang efisien karena robot crawler dari search engine kadang perlu membaca keseluruhan halaman. Sehingga bisa saja robot crawler akan fetch api halaman selanjutnya saat sampai bawah yang mengakibatkan score SEO kita turun. Selain itu, ketika user scroll menuju record ke-10 pada halaman, aplikasi tersebut akan otomatis melakukan fetch halaman selanjutnya karena record ke-10 berada pada urutan paling bawah sehingga event onscroll
tereksekusi. Tentu ini juga kurang efektif karena aplikasi melakukan fetch halaman yang tidak diperlukan user.
Tombol "Load More" maupun "Next-Previous" konsepnya sama, hanya beda di User Interface saja. Ini alternatif dari Infinite Scroll. Cara kerjanya mirip, yaitu dengan cara fetch halaman selanjutnya tanpa perlu menghitung total record yang ada. Bedanya, ini ga pakai event onscroll
, melainkan fetch melalui tombol “Load More” untuk melihat halaman selanjutnya atau menggunakan tombol "Next/Previous" untuk navigasi melihat halaman selanjutnya dan halaman sebelumnya pada web. Jadi footernya tetap kelihatan dan juga ga perlu takut score SEO turun. Fetch halaman selanjutnya hanya akan terjadi jika user menginginkannya lewat tombol tersebut. Di sini user perlu effort dengan melakukan klik tombol dibandingkan Infinite Scroll yang effortless tinggal scroll.
<html lang="id">
<body style="font-size:20px;height:1500px">
<li>
<!-- your list of records here-->
</li>
<button onclick="myButtonFunction()">load more</button>
</body>
</html>
<script>
function myButtonFunction() {
fetch("https://your.api/here")
.then(o => o.json())
.then(o => {
//do yor things here...
})
}
</script>
Untuk versi mobile, Google ga pakai Numeric Pagination. Tapi untuk versi web, Google masih menggunakan Numeric Pagination. Memang benar. Tapi, Google tidak melakukan counting total record ketika kita melakukan pencarian. Melainkan Google hanya menampilkan Top 20 record untuk satu halaman, dan Google juga membatasi paging hanya sampai 20an halaman yang menurut mereka relevan.
Setelah itu Google menyarankan untuk melanjutkan query pencarian dengan menyertakan hasil yang disembunyikan sampai pada halaman 30an. Itu adalah halaman terakhir yang bisa kita capai bersama Google. Jika masih belum ditemukan apa yang kita cari, maka kita harus mengubah query pencarian yang lebih spesifik. Itulah flow yang dilakukan Google saat ini ketika tulisan ini dibuat.
Google juga menampilkan total record pada bagian atas pencarian. Tapi itu bukan berarti Google menghitung query total record saat itu juga. Melainkan itu adalah hasil perkiraan mereka yang mereka kumpulkan pada database analytics mereka. Makanya hasilnya juga bukan angka spesifik, tapi pembulatan.
Betul, pada postingan ini gw menentang Numeric Pagination. Tapi hingga saat tulisan ini dirilis, gw masih menggunakan Numeric Pagination pada blog ini🤭. Well, blog ini adalah website statis. Ga ada query atau koneksi database untuk mendapatkan list tulisan yang gw buat. Hanyalah HTML biasa yang di-load di setiap halamannya. HTML tersebut selalu di-build ulang tiap ada tulisan baru dirilis untuk menentukan isi masing-masing page. Jadi ga ngaruh sama sekali saat load. Kemudian, total tulisan yang pernah gw rilis hingga saat ini ga banyak. Seperti yang sudah gw jelaskan di atas, Numeric Pagination itu terasa dampaknya ketika total record yang dihitung sudah banyak. Tapi misalkan suatu saat nanti gw migrasi blog ini menggunakan database dan tulisan gw udah banyak, pastinya akan gw ganti.
Sejauh ini, Numeric Pagination udah ketinggalan jaman. Hanya cocok diterapkan untuk aplikasi statis atau dengan data yang masih sedikit. Secara teknis tidak efektif untuk aplikasi yang menggunakan data dinamis, karena melakukan minimal 2x query untuk satu aktivitas. Numeric Pagination itu juga lambat jika data yang disimpan sudah banyak. User juga jadi bisa mengakses halaman ke-sejuta yang yang bisa berdampak pada performa database. Secara fitur juga useless, karena hampir ga ada user yang mau ngecekin masing-masing page satu-persatu dari page satu sampai akhir. User lebih suka apply filter untuk hasil yang spesifik. Google web walaupun masih pakai Numeric Pagination tapi tidak menampilkan semua hasil databasenya, melainkan hanya 20an hingga 30an halaman saja. Aplikasi jaman sekarang sudah berevolusi menggunakan alternatif yang lebih baik. Date Range Limitation lebih baik karena membatasi hasil pencarian berdasarkan tanggal. Secara User Experience, Infinite Scroll lebih baik karena effortless, aplikasi otomatis melakukan fetch halaman selanjutnya ketika scroll. Secara efisiensi, tombol Load More atau Next-Previous lebih efisien karena fetch halamannya on-demand. Atau selain itu, ga ada salahnya menggabungkan antara Infinite Scroll, Load More ataupun Next-Previous dengan Date Range Limitation😎.