Virtual Scrolling — Performance

Virtual Scrolling Virtual scrolling (windowing) adalah teknik untuk merender hanya item yang terlihat di viewport, meskipun list memiliki ribuan item. Masalah…

Virtual scrolling (disebut juga windowing) adalah teknik merender cuma baris yang kelihatan dari daftar yang panjang, walaupun datanya ribuan. Bayangin naik kereta: pemandangan di luar panjangnya berkilo-kilometer, tapi yang lewat di jendela cuma sepotong. Jendela gak perlu memuat seluruh pemandangan, cukup potongan yang ada di depannya sekarang. Waktu kereta jalan, potongannya berganti.

🧪 Tebakan cepat: list berisi 1.000 baris, tiap baris tingginya 50 px, dan kotak list-nya setinggi 500 px. Dengan virtual scrolling, berapa elemen baris yang ada di DOM: 10, 11, atau 1.000? Jawabannya ada di bagian "Menghitung baris yang dirender".

Masalahnya: semua baris masuk DOM

Kalau 10.000 baris dirender semua, browser harus membuat 10.000 elemen, memberi style, menghitung layout, dan menyimpan semuanya di memori. Padahal di kotak 500 px, yang kelihatan cuma sekitar 10 baris. Halaman jadi lambat muncul, boros memori, dan tiap kali layout dihitung ulang, 10.000 elemen itu ikut dihitung.

Cara kerjanya

Virtual scrolling memisahkan dua hal: tinggi yang dirasakan scrollbar, dan baris yang benar-benar ada.

  1. Kotak scroll punya tinggi tetap (500 px) dan overflow-y: auto. Posisi scroll-nya dibaca dari scrollTop, yaitu berapa px isinya sudah tergulung ke atas.
  2. Spacer, elemen kosong setinggi semua baris: 1.000 × 50 = 50.000 px. Berkat dia, scrollbar-nya terasa seperti list penuh.
  3. Baris yang kelihatan saja, dibungkus satu elemen yang digeser turun sejauh offsetY dengan transform: translateY(...), supaya baris itu muncul tepat di posisi scroll.
  4. Tiap event scroll, hitung ulang barisnya dari scrollTop, lalu ganti isinya.

Menghitung baris yang dirender

scrollTop startIndex endIndex offsetY visibleCount Baris di DOM
0 0 11 0 11 0 sampai 10
2500 50 61 2500 11 50 sampai 60
2525 50 61 2500 11 50 sampai 60
49500 990 1000 49500 10 990 sampai 999

Dari 1.000 data, cuma 11 baris yang ada di DOM. Itu jawaban tebakannya: 11. Dengan 10.000 data pun tetap 11. Di baris terakhir tabel, 990 + 11 = 1001 melewati 1.000, jadi Math.min memotongnya ke 1000 dan cuma 10 baris yang dirender.

function getVisibleItems(scrollTop, containerHeight, itemHeight, totalItems) {
  const startIndex = Math.floor(scrollTop / itemHeight);
  const rows = Math.ceil(containerHeight / itemHeight) + 1; // +1 baris cadangan
  const endIndex = Math.min(startIndex + rows, totalItems);

  return {
    startIndex,
    endIndex,
    offsetY: startIndex * itemHeight,
    visibleCount: endIndex - startIndex,
  };
}

console.log(getVisibleItems(2500, 500, 50, 1000));
// { startIndex: 50, endIndex: 61, offsetY: 2500, visibleCount: 11 }

Menyambungkannya ke halaman

const list = document.querySelector("#list"); // tinggi 500px, overflow-y: auto
const rows = document.querySelector("#rows"); // di dalam spacer setinggi 50.000px

function render() {
  const { startIndex, endIndex, offsetY } = getVisibleItems(list.scrollTop, 500, 50, 1000);
  const fragment = document.createDocumentFragment();
  for (let i = startIndex; i < endIndex; i++) {
    const row = document.createElement("div");
    row.className = "row"; // height: 50px
    row.textContent = "Baris " + i;
    fragment.appendChild(row);
  }
  rows.replaceChildren(fragment);
  rows.style.transform = `translateY(${offsetY}px)`;
}

list.addEventListener("scroll", render);
render();

Kapan dipakai

Pakai untuk daftar ratusan sampai jutaan baris: tabel log, feed, riwayat chat, spreadsheet. Untuk 30 atau 50 baris, render biasa sudah cukup. Virtual scrolling menambah kerumitan, dan baris yang gak ada di DOM gak bisa ditemukan lewat Ctrl+F browser.

⚠️ Jebakan yang sering ketemu

TL;DR: Virtual scrolling cuma merender baris yang kelihatan, ditambah sedikit cadangan, di dalam spacer setinggi semua baris supaya scrollbar-nya tetap benar. Rumusnya: startIndex = Math.floor(scrollTop / itemHeight), endIndex = Math.min(startIndex + Math.ceil(containerHeight / itemHeight) + 1, totalItems), offsetY = startIndex * itemHeight, visibleCount = endIndex - startIndex. Kotak 500 px dengan baris 50 px cuma butuh 11 baris di DOM, mau datanya 1.000 atau 10.000. Syaratnya, tinggi tiap baris konsisten atau bisa diukur. Untuk list pendek, render biasa saja.

Yang akan kamu pelajari