
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.
Cara Kerja
Kita perlu tahu dulu cara kerja pagination tersebut. Dari Frontend akan memberikan parameter untuk menentukan halaman ke berapa yang ingin ditampilkan, kita sebut saja nama parameternya di sini sebagai pageNo
. Ditambah satu parameter lain untuk menentukan batas record pada satu halaman, kita sebut saja nama parameternya pageSize
. Jadi misalkan parameternya 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 parameter. Untuk melakukan paging, kita bisa menggunakan keyword OFFSET
pada sql. Offset pada sql fungsinya adalah untuk melewatkan beberapa record pertama. Misalkan Offset = 20, itu artinya 20 record pertama akan di-skip pada hasil. 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
. Begitu seterusnya. Query yang akan dieksekusi kurang lebih sebagai berikut:
-- pageNo=1&pageSize=10
SELECT o.order_no, o.note, o.ordering_date
FROM orders o
ORDER BY o.ordering_date
LIMIT 10 OFFSET 0
;
-- pageNo=2&pageSize=10
SELECT o.order_no, o.note, o.ordering_date
FROM orders o
ORDER BY o.ordering_date
LIMIT 10 OFFSET 10
;
-- pageNo=7&pageSize=10
SELECT o.order_no, o.note, o.ordering_date
FROM orders o
ORDER BY o.ordering_date
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. 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 - 1) / 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 - 1) / 10) + 1) = 4
. Jadi dari Backend selain menampilkan list record, juga menampilkan total page pada response, yaitu 4. Nanti di Frontend akan menampilkan urutan halaman sebanyak value total page dari response yang ketika diklik akan menampilkan halaman sesuai nomor. Itulah yang disebut Numeric Pagination.
Ketika total recordnya ga banyak, kurang lebih 100ribu record mungkin belum 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. Itu karena sql akan mengurutkan data dari halaman pertama hingga halaman ke-n yang dipilih sesuai offset. Misalkan user memilih halaman terakhir atau data ke-sejuta, maka itu bisa berdampak pada penurunan performa aplikasi😱. Melakukan select count(*)
untuk jutaan data juga akan menguras kinerja database. Query-nya akan sangat lambat. Selain itu, Numeric Pagination kayak gini cenderung ga 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 solusinya?
Date Range Limitation
Ini adalah cara paling umum yang dilakukan oleh perbankan. Contohnya pada menu mutasi rekening internet banking atau mobile banking. User 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 lebih dari ratusan ribu record. Selain itu kolom tanggal pada database sebaiknya di-index untuk optimasi filter date range.
-- pageNo=3&pageSize=10 from 01 january 2022 to 31 january 2022
SELECT o.order_no, o.note, o.ordering_date
FROM orders o
WHERE o.ordering_date BETWEEN 20220101 and 20220131
ORDER BY o.ordering_date
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
;
Kelebihan Date Range Limitation
Kita hanya perlu menambah satu filter tanggal doang di query. Pagination tetap ada tapi data yang diproses akan lebih sedikit dibanding tanpa filter tanggal. Sisanya tetap seperti sebelumnya, ga perlu perubahan lain.
Kekurangan Date Range Limitation
Ini hanya berlaku jika data dalam range tanggal tersebut ga banyak. Kalau datanya banyak tetap akan lambat performanya.
Infinite Scroll
Alternatifnya adalah menggunakan Lazy Load with Infinite Scroll. Pendekatan ini paling sering ditemui pada aplikasi modern seperti timeline sosial media, order makanan online, riwayat saldo e-wallet, dan lainnya. Kita ambil contohnya YouTube. Ketika kita melakukan search video, kita akan ditampilkan 25 video pertama. Lalu ketika kita scroll sampai bawah, nanti akan muncul loading icon di bawah untuk fetch halaman kedua. Begitu seterusnya setiap scroll sampai bawah hingga pada halaman terakhir sudah tidak ada data lagi. Frontend perlu bikin event onscroll
untuk fetch data setiap user scroll halaman sampai bawah.
window.onscroll = function() { fetchMorePage() };
function fetchMorePage() {
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 your things here...
})
}
}
Kelebihan Infinite Scroll
Ini lebih baik karena dengan ini minimal kita hanya sekali query. Query yang dieksekusi hanya untuk mendapatkan n 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. Untuk hasil spesifik user bisa apply filter.
Kekurangan Infinite Scroll
Jika ada footer, user jadi kesulitan mengaksesnya karena akan ada fetch data tiap user scroll sampai bawah. Lalu, jika membutuhkan 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 trigger event untuk fetch api halaman selanjutnya saat sampai bawah yang berdampak pada score SEO. Selain itu, ketika user scroll menuju record terakhir pada halaman yang dibuka, maka aplikasi akan otomatis melakukan fetch halaman selanjutnya karena record tersebut berada pada urutan bawah sehingga event onscroll
ter-trigger. Tentu ini juga kurang efisien karena aplikasi melakukan fetch halaman yang tidak diperlukan user.
Tombol “Load More” atau “Next/Previous”
Tombol “Load More” maupun “Next/Previous” konsepnya sama, hanya beda tampilan saja. Contohnya pada website GitHub di bagian riwayat commit yang menggunakan tombol “Next/Previous”. Atau pada index situs berita yang biasanya menggunakan tombol “Load More”. 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/sebelumnya.
<html lang="id">
<body>
<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 your things here...
})
}
</script>
Kita juga perlu pastikan bahwa tombol “Load More” atau “Next” hanya bisa dipencet saat data selanjutnya ada. Untuk itu dari Backend perlu ngasih property tambahan pada response, misalnya hasNextPage
berupa boolean value untuk memberi tahu Frontend bahwa jika value-nya false
maka tombolnya di-disable dan jika true
baru bisa dipencet. Caranya dengan mengganti value limit dengan pageSize + 1
saat query.
-- pageNo=1&pageSize=10
SELECT o.order_no, o.note, o.ordering_date
FROM orders o
ORDER BY o.ordering_date
LIMIT 11 OFFSET 0
;
Jika jumlah data yang muncul kurang dari limit tersebut maka property hasNextPage
akan di-set false
, selain itu true
. Lalu perlu logic tambahan dari sisi aplikasi untuk membuang record terakhir dari hasil query agar data yang muncul di response jumlahnya sesuai pageSize
pada request parameter.
Kelebihan Tombol “Load More” atau “Next/Previous”
Sama seperti Infinite Scroll, eksekusi query halaman yang offsetnya gede bisa dihindari. Footernya tetap kelihatan dan ga perlu takut score SEO turun saat diakses web crawler. Fetch halaman selanjutnya hanya akan terjadi jika user menginginkannya lewat tombol tersebut sehingga lebih efisien.
Kekurangan Tombol “Load More” atau “Next/Previous”
Di sini user perlu effort dengan melakukan klik tombol dibandingkan Infinite Scroll yang effortless, tinggal scroll doang. Perlu property tambahan pada response dari Backend untuk memberi tahu Frontend apakah ada record selanjutnya agar tombol tersebut bisa dipencet.
Filter By Last Identifier
Jika halaman tersebut datanya diurutkan berdasarkan ID yang unik, maka dengan salah satu alternatif di atas kita bisa improve lagi dengan cara filter by ID terakhir dari data sebelumnya. Misalkan dari 10 data pertama yang ditampilkan, ID ke-10 dari data tersebut adalah 777
. Maka saat fetch halaman kedua Frontend perlu mengirim parameter tambahan, misalnya lastIdentifier=777
. Nanti di Backend akan melakukan filter tambahan berdasarkan ID tersebut.
-- pageSize=10&lastIdentifier=777
SELECT o.order_no, o.note, o.ordering_date
FROM orders o
WHERE o.order_no > 777
ORDER BY o.order_no
LIMIT 10
;
Ini dengan asumsi datanya diurutkan secara ascending. Jika secara descending maka filternya jadi o.order_no < 777
.
Kelebihan Filter By Last Identifier
Di sini ga ada offset sama sekali, sehingga secara performa akan lebih cepat lagi. Kita hanya perlu tambahan filter by ID terakhir dari data sebelumnya saat query data selanjutnya.
Kekurangan Filter By Last Identifier
Ini hanya berlaku jika data yang ditampilkan diurutkan berdasarkan ID yang unik. Jika data yang ditampilkan diurutkan berdasarkan kolom lain maka akan ribet. Kita jadi harus tambahin filter by ID terakhir dan value terakhir kolom lain yang dipake untuk sorting pada request parameter. Apalagi kalau kolom yang dipake untuk sorting lebih dari satu, tentu makin ribet lagi. Juga ga bisa jika data tidak diurutkan dengan minimal satu kolom unik di akhir order by.
Optimasi Query
Selain menggunakan cara-cara di atas, perlu dipastikan juga query yang dieksekusi sudah optimal. Bisa jadi penyebab lambatnya bukan karena jumlah data, tapi query yang perlu dioptimasi. Pastikan juga kolom pada ORDER BY
dan WHERE
clause di-index seperlunya. Ini juga ngaruh ke performa. Cara-cara di atas tetap akan lambat jika kolom-kolom yang dipake untuk ORDER BY
ga di-index sehingga database mengurutkan datanya secara O(n * log(n))
. Sedangkan jika di-index menggunakan BTree maka database akan mengurutkan data secara O(log(n))
sehingga lebih cepat.
Bagaimana dengan Google?
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. Google membatasi akses data website yang mereka miliki untuk satu query pencarian. Google hanya menampilkan Top 20 record untuk satu halaman dan paging-nya 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 secara real time. Melainkan itu adalah hasil perkiraan dari database analytics mereka. Makanya hasilnya juga bukan angka spesifik, tapi angka perkiraan.
Bagaimana dengan Blog Ini?
Betul, pada postingan ini gw menentang Numeric Pagination. Tapi hingga saat tulisan ini dirilis, gw masih menggunakan Numeric Pagination pada index blog ini🤭. Well, blog ini adalah website statis. Ga ada query atau koneksi database saat diakses 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 diakses. 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.
Verdict
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, yaitu select data dan select count. Numeric Pagination itu akan lambat jika data yang disimpan sudah banyak. User 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 saat jumlah pagenya terlalu banyak. User lebih suka apply filter untuk hasil yang spesifik. Google web walaupun masih pakai Numeric Pagination tapi tidak menampilkan semua hasil databasenya pada pagination, 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 range tanggal tertentu. 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. Jika data yang ditampilkan diurutkan berdasarkan ID, maka bisa di-improve lagi dengan menambahkan filter by ID terakhir dari data sebelumnya. Selain itu, ga ada salahnya menggabungkan antara Infinite Scroll, tombol “Load More” ataupun “Next/Previous” dengan filter ID terakhir dan Date Range Limitation😎.