Episode ini membedah TanStack Virtual: konsep windowing dan useVirtualizer, virtual scrolling untuk list dan tabel, pengukuran item dinamis dengan measureElement, serta performance tuning seperti overscan untuk data dalam jumlah besar.

Merender list dengan seratus ribu item adalah hukuman bagi browser: setiap item butuh node DOM, dan setiap scroll memaksa layout ulang. TanStack Virtual menyelesaikan ini dengan windowing — hanya item yang terlihat dalam viewport yang benar-benar dirender.
Episode 11 membedah konsep windowing dan useVirtualizer, virtual scrolling untuk list dan tabel, pengukuran item dengan tinggi dinamis, serta tuning performa dengan overscan dan estimasi ukuran.
Ini pola yang menyelamatkan aplikasi dashboard dan log viewer dari degradasi performa. Di episode 9 kalian sudah menyentuh virtualized table; sekarang kita dalami mekanismenya.
Virtualizer menghitung item mana yang masuk viewport berdasarkan posisi scroll, lalu menyediakan rentang item tersebut. Sisa ruang diisi placeholder setinggi total data:
import { useRef } from "react"
import { useVirtualizer } from "@tanstack/react-virtual"
function Daftar() {
const parentRef = useRef(null)
const virtualizer = useVirtualizer({
count: 100_000,
getScrollElement: () => parentRef.current,
estimateSize: () => 40,
})
const items = virtualizer.getVirtualItems()
return (
<div ref={parentRef} style={{ height: 400, overflowY: "auto" }}>
<div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
{items.map((item) => (
<div
key={item.key}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: item.size,
transform: `translateY(${item.start}px)`,
}}
>
Baris ke-{item.index}
</div>
))}
</div>
</div>
)
}getScrollElement: () => parentRef.current menunjuk ke elemen scroll. getVirtualItems() mengembalikan item terlihat; setiap item ditempatkan dengan translateY sebesar start. getTotalSize() memberi tinggi total sehingga scrollbar mengikuti 100 ribu baris meskipun hanya belasan yang dirender.
Untuk tabel, virtualisasi biasanya diterapkan pada baris (di episode 9) dan bisa juga pada kolom. Baris virtual mengambil data dari table.getRowModel().rows, sehingga sorting dan filtering tetap berjalan sebelum virtualisasi:
const rows = table.getRowModel().rows
const virtualRows = virtualizer.getVirtualItems()
const totalSize = virtualizer.getTotalSize()
return (
<tbody>
{virtualRows.map((virtualRow) => {
const row = rows[virtualRow.index]
return (
<tr key={row.id} style={{ height: virtualRow.size }}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
)
})}
</tbody>
)virtualRows berisi baris yang terlihat, dan tiap item memetakan ke baris tabel asli lewat index. Row model tetap menjadi sumber kebenaran, sementara virtualizer menentukan mana yang masuk DOM.
Ketika item punya tinggi berbeda, estimasi statis tidak akurat. Gunakan measureElement agar virtualizer mengukur tinggi nyata setelah render:
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 80,
measureElement: (el) => el.getBoundingClientRect().height,
})
<div
ref={virtualizer.measureElement}
data-index={item.index}
style={{ transform: `translateY(${item.start}px)` }}
>
{items[item.index]}
</div>ref={virtualizer.measureElement} ditambahkan ke tiap item bersama data-index. Setelah item dirender, virtualizer mengukur tinggi aktual dan mengoreksi posisi item berikutnya. Scroll jadi akurat meskipun konten tiap item berbeda-beda panjangnya.
Dua pengaturan kunci untuk performa:
overscan: jumlah item ekstra yang dirender di luar viewport. Nilai kecil memangkas kerja React; nilai besar mencegah flash saat scroll cepat.estimateSize: gunakan estimasi yang mendekati ukuran nyata agar perhitungan posisi awal akurat.const virtualizer = useVirtualizer({
count: 100_000,
getScrollElement: () => parentRef.current,
estimateSize: () => 40,
overscan: 8,
scrollMargin: parentRef.current?.offsetTop ?? 0,
})overscan: 8 merender delapan item tambahan di tiap arah. scrollMargin penting jika area scroll tidak dimulai dari atas halaman — tanpa ini, posisi item salah saat ada elemen di atas container.
Warning
Jangan memasang ref={virtualizer.measureElement} pada komponen yang sering berubah ukuran, seperti gambar yang di-load lambat. Ukur dengan ResizeObserver terpisah agar virtualizer tidak diukur ulang terus-menerus.
Episode 11 menutup virtualisasi: windowing dengan useVirtualizer yang hanya merender item terlihat, virtual scrolling untuk list dan tabel, pengukuran item dinamis lewat measureElement, serta tuning dengan overscan dan estimasi realistis.
Inti yang harus dibawa pulang:
Di episode 12 selanjutnya kita akan membahas authentication dan secure data fetching — penyimpanan token yang aman, auth headers pada query client, refresh token flow, router guards, serta role-based data fetching. Data kalian mulai butuh pengamanan!