10.000 satırlık bir liste render etmeniz gerektiğinde tarayıcı neden donuyor? Cevap basit: virtualization. Bu yazıda, React uygulamalarında büyük veri setlerini akıcı şekilde render etmek için kullanılan virtualization tekniğini, örneklerle ele alacağız.
Diyelim ki içerisinde liste olan bir uygulama geliştiriyoruz ve 15.000 liste elemanı var (Örn: Instagram veya Twitter’daki gibi postların listelendiği timeline olan bir uygulama ya da çok fazla ürünnün listelendiği bir e-ticaret sitesi vb). Kullanıcı arama yaptığında tüm bu item’ları bir liste halinde göstermek istiyoruz.
İlk akla gelen çözüm şu:
// ❌ Bu yaklaşım performans cehennemi
function Timeline({ tweets }) {
return (
<ul>
{tweets.map((tweet) => (
<li key={tweet.id}>
<TweetCard tweet={tweet} />
</li>
))}
</ul>
);
} Burada ne oluyor? 15.000 DOM node’u aynı anda yaratılıyor. Her biri memory’de yer kaplıyor, her state değişikliğinde reconciliation sürecine giriyor ve tarayıcı layout hesaplamalarını tüm bu node’lar için tekrar yapıyor.
Chrome DevTools’u açtığımızda şunu görürüz: sayfa ilk yüklendiğinde bile yüzlerce milisaniye harcıyor. Scroll etmeye başladığımızda frame rate 60fps’den 10–15fps’e düşüyor. Kullanıcı deneyimi açısından bu, uygulamanın kullanılamaz hale geldiği anlamına geliyor.
🔍 Virtualization Nasıl Çalışır?
Virtualization (veya diğer adıyla windowing), yalnızca görünür öğelerin render edilmesi prensibine dayanır.
Virtualization’ın temel fikri: kullanıcı neyi görebiliyorsa yalnızca onu render et.
Viewport’ta 10 item görünüyorsa, DOM’da yalnızca 10–15 node olsun. Kullanıcı scroll ettikçe görünen node’ların içeriği güncelleniyor; yeni node’lar oluşturulmuyor. Liste container’ının toplam yüksekliği hesaplanıyor ve bu yükseklik korunuyor, böylece scroll bar doğru orantıda çalışıyor.
Bu yaklaşımda DOM node sayısı sabit kalıyor. 15.000 item olsa da DOM’da en fazla 20–30 node görünüyor.
📦 TanStack Virtual
TanStack Virtual, headless bir virtualization kütüphanesidir.
📝 Benim uzun süredir bildiğim react-virtualized kütüphanesi var. Ancak şuan en güncel olan TanStack’in kütüphaneleri gibi görünüyor, bu yüzden ben makaleyi onun üzerinden hazırladım.
Kurulumla başlayalım:
npm install @tanstack/react-virtual Basit Bir Virtual Liste Oluşturalım
İlk örneğimizde sabit yükseklikli satırlardan oluşan bir liste yapalım.
import { useRef } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
function VirtualList({ items }) {
// Scroll container'a ihtiyacımız var
const parentRef = useRef(null);
const rowVirtualizer = useVirtualizer({
count: items.length, // Toplam öğe sayısı
getScrollElement: () => parentRef.current,
estimateSize: () => 64, // Her satırın tahmini yüksekliği (px)
overscan: 5, // Viewport dışında extra render edilen öğe sayısı
});
return (
// Scroll edilebilir container
<div
ref={parentRef}
style={{
height: "600px",
overflow: "auto",
}}
>
{/* Toplam liste yüksekliği — scroll bar için gerekli */}
<div
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
width: "100%",
position: "relative",
}}
>
{rowVirtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{items[virtualItem.index].name}
</div>
))}
</div>
</div>
);
} - count: Toplam kaç öğe olduğunu söylüyoruz; hepsini render etmiyoruz, sadece sayısını bildiriyoruz.
- estimateSize: Başlangıçta her öğenin kaç pixel yükseklikte olduğunu tahmin ediyoruz.
- overscan: Viewport'un hemen dışında kaç öğenin "hazır" tutulacağını belirliyor; scroll sırasında flash (beyaz alanın görünmesi) önleniyor.
- getTotalSize(): Tüm liste render edilseydi kaç pixel yükseklikte olurdu? Bu değeri container'a veriyoruz ki scroll bar doğru çalışsın.
- transform: translateY(...): Her öğeyi absolute pozisyonla doğru konuma taşıyoruz.
📐 Dinamik Yükseklikli Öğeler
Her satır aynı yükseklikte olmayabilir. Açıklaması uzun ürünler, farklı boyutlu resimler, expand/collapse içeriği olan kartlar vs.
TanStack Virtual bunu measureElement callback'i ile çözüyor.
import { useRef, useCallback } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
function DynamicVirtualList({ items }) {
const parentRef = useRef(null);
const rowVirtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 100, // Başlangıç tahmini
measureElement:
typeof window !== "undefined" &&
navigator.userAgent.indexOf("Firefox") === -1
? (element) => element?.getBoundingClientRect().height
: undefined,
});
return (
<div ref={parentRef} style={{ height: "600px", overflow: "auto" }}>
<div
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
width: "100%",
position: "relative",
}}
>
{rowVirtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
// measureElement'in çalışması için data-index gerekli
data-index={virtualItem.index}
// ref ile gerçek DOM yüksekliği ölçülüyor
ref={rowVirtualizer.measureElement}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
transform: `translateY(${virtualItem.start}px)`,
}}
>
<ProductCard product={items[virtualItem.index]} />
</div>
))}
</div>
</div>
);
} Buradaki kritik fark şu:
- ref={rowVirtualizer.measureElement}: Her öğe DOM'a mount olduğunda gerçek yüksekliği ölçülüyor ve virtualizer bu bilgiyle tüm hesaplamalarını güncelliyor.
- data-index={virtualItem.index}: Virtualizer hangi öğeyi ölçtüğünü bu attribute üzerinden anlıyor.
- Firefox kontrolü: Firefox’ta getBoundingClientRect() bazı edge case'lerde farklı sonuçlar verebiliyor; bu yüzden özellikle dışarıda tutuluyor.
🔲 Grid Virtualization
Bazen liste değil, grid yapısına ihtiyacımız oluyor. Fotoğraf galerisi, ürün kartılarından oluşan gridler gibi senaryolarda hem yatay hem dikey eksende virtualization gerekiyor.
TanStack Virtual’da grid için hem satır hem sütun virtualizer’larını birlikte kullanıyoruz:
import { useRef } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
const COLUMN_COUNT = 4;
const CARD_HEIGHT = 280;
const CARD_WIDTH = 250;
function VirtualGrid({ items }) {
const parentRef = useRef(null);
// Satır sayısını sütun sayısına göre hesaplıyoruz
const rowCount = Math.ceil(items.length / COLUMN_COUNT);
const rowVirtualizer = useVirtualizer({
count: rowCount,
getScrollElement: () => parentRef.current,
estimateSize: () => CARD_HEIGHT,
overscan: 2,
});
const columnVirtualizer = useVirtualizer({
horizontal: true, // Yatay virtualization aktif
count: COLUMN_COUNT,
getScrollElement: () => parentRef.current,
estimateSize: () => CARD_WIDTH,
overscan: 1,
});
return (
<div
ref={parentRef}
style={{
height: "700px",
overflow: "auto",
}}
>
<div
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
width: `${columnVirtualizer.getTotalSize()}px`,
position: "relative",
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) =>
columnVirtualizer.getVirtualItems().map((virtualColumn) => {
// Hangi öğeye denk geldiğini hesaplayalım
const itemIndex = virtualRow.index * COLUMN_COUNT + virtualColumn.index;
// Öğe yoksa boş döndür (son satırda eksik kart olabilir)
if (itemIndex >= items.length) return null;
return (
<div
key={`${virtualRow.index}-${virtualColumn.index}`}
style={{
position: "absolute",
top: 0,
left: 0,
width: `${virtualColumn.size}px`,
height: `${virtualRow.size}px`,
transform: `translateX(${virtualColumn.start}px) translateY(${virtualRow.start}px)`,
}}
>
<ProductCard product={items[itemIndex]} />
</div>
);
})
)}
</div>
</div>
);
} - horizontal: true: Column virtualizer'a yatay eksende çalışması gerektiğini söylüyoruz.
- İki virtualizer’ı iç içe map ederek 2D koordinat sistemi kuruyoruz.
- translateX + translateY ile her kartı doğru konuma taşıyoruz.
- Son satırda eksik kart olabileceği için index sınırı kontrol ediyoruz.
♾️ Infinite Scroll ile Virtualization
En sık karşılaştığımız senaryo, sayfalama yerine infinite scroll ama aynı zamanda performanslı bir liste. Bu ikisini birleştirmek için TanStack Query + TanStack Virtual ikilisini kullanıyoruz.
import { useRef, useEffect, useCallback } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
import { useInfiniteQuery } from "@tanstack/react-query";
const PAGE_SIZE = 20;
async function fetchProducts({ pageParam = 0 }) {
const response = await fetch(
`/api/products?offset=${pageParam}&limit=${PAGE_SIZE}`
);
return response.json();
}
function InfiniteVirtualList() {
const parentRef = useRef(null);
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery({
queryKey: ["products"],
queryFn: fetchProducts,
getNextPageParam: (lastPage, allPages) =>
lastPage.hasMore ? allPages.length * PAGE_SIZE : undefined,
});
// Tüm sayfaları tek bir array'de birleştiriyoruz
const allItems = data?.pages.flatMap((page) => page.items) ?? [];
// Sonunda bir "loading" satırı için +1
const totalCount = hasNextPage ? allItems.length + 1 : allItems.length;
const rowVirtualizer = useVirtualizer({
count: totalCount,
getScrollElement: () => parentRef.current,
estimateSize: () => 80,
overscan: 5,
});
const virtualItems = rowVirtualizer.getVirtualItems();
// Son öğeye yaklaştığımızda yeni sayfa yükle
useEffect(() => {
const lastItem = virtualItems[virtualItems.length - 1];
if (!lastItem) return;
if (
lastItem.index >= allItems.length - 1 &&
hasNextPage &&
!isFetchingNextPage
) {
fetchNextPage();
}
}, [virtualItems, allItems.length, hasNextPage, isFetchingNextPage, fetchNextPage]);
return (
<div ref={parentRef} style={{ height: "600px", overflow: "auto" }}>
<div
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
width: "100%",
position: "relative",
}}
>
{virtualItems.map((virtualItem) => {
const isLoaderRow = virtualItem.index > allItems.length - 1;
return (
<div
key={virtualItem.key}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{isLoaderRow ? (
<div className="loading-spinner">Yükleniyor...</div>
) : (
<ProductRow product={allItems[virtualItem.index]} />
)}
</div>
);
})}
</div>
</div>
);
} Bu pattern’de dikkat etmemiz gereken kısımlar şunlar:
- flatMap: Her sayfa ayrı bir array olarak geliyor, hepsini birleştirerek tek bir flat liste elde ediyoruz.
- totalCount + 1: Eğer daha sayfa varsa, listenin sonuna bir "ghost" satır ekliyoruz. Bu satır loading spinner'ı gösteriyor.
- useEffect içindeki intersection kontrolü: Son virtual item'ın index'i gerçek öğe sayısına yaklaştığında fetchNextPage() tetikleniyor. Kullanıcı scroll'u bitirmeden yeni veri hazır oluyor.
🔄 Belirli Bir Öğeye Scroll Etmek
“Sepete eklendi, ürüne git” veya “arama sonucuna atla” gibi senaryolarda belirli bir öğeye programatik olarak scroll etmemiz gerekiyor.
import { useRef } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
function ScrollableList({ items, highlightedIndex }) {
const parentRef = useRef(null);
const rowVirtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 64,
});
// Belirli bir öğeye scroll etmek için
const scrollToItem = (index) => {
rowVirtualizer.scrollToIndex(index, {
align: "start", // "start" | "center" | "end" | "auto"
behavior: "smooth",
});
};
return (
<div>
<button onClick={() => scrollToItem(highlightedIndex)}>
Seçili ürüne git
</button>
<div ref={parentRef} style={{ height: "600px", overflow: "auto" }}>
<div
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
position: "relative",
}}
>
{rowVirtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
backgroundColor:
virtualItem.index === highlightedIndex
? "#1a1a2e"
: "transparent",
}}
>
{items[virtualItem.index].name}
</div>
))}
</div>
</div>
</div>
);
} scrollToIndex metodunun parametreleri:
- align: "start": Öğeyi viewport'un üst kenarına hizala.
- align: "center": Öğeyi tam ortaya getir; highlight senaryoları için ideal.
- align: "auto": Öğe zaten görünüyorsa scroll etme, değilse minimum scroll yap.
- behavior: "smooth": Native smooth scroll davranışı; kullanıcı nereye gittiğini anlayabilsin.
❓ Ne Zaman Virtualization Kullanılmalı?
Virtualization özellikle büyük veri setleriyle çalışırken ciddi performans kazancı sağlar. Ancak her liste için gerekli değildir. Yanlış yerde kullanıldığında gereksiz karmaşıklık oluşturabilir.
Genellikle şu senaryolarda virtualization tercih edilir:
- Binlerce öğeden oluşan listeler
- Infinite scroll kullanılan timeline yapıları
- Büyük ürün katalogları
- Admin paneli tabloları
- Veri yoğun dashboard ekranları
- Fotoğraf galerileri ve grid yapıları
Özellikle aynı anda yüzlerce veya binlerce DOM node’un render edildiği durumlarda virtualization ciddi fark yaratır.
⚠️ Ne Zaman Virtualization Kullanılmamalı?
Bazı durumlarda virtualization gereksiz olabilir:
- 50–100 öğelik küçük listeler
- SEO açısından önemli içerikler
- Çok basit ve statik tablolar
💡 Unutmayalım ki, virtualization’ın da bir maliyeti vardır. Ek hesaplamalar, ölçümler ve scroll yönetimi nedeniyle kod karmaşıklığını artırabilir. Bu yüzden gerçekten ihtiyaç olan yerlerde kullanmak daha doğru bir yaklaşım olacaktır.
🏁 Sonuç
Virtualization, büyük veri setleriyle çalışan her projede uygulayabileceğimiz temel bir pattern. DOM node sayısını sabit tutarak bellek kullanımını dramatik şekilde azaltıyor.
Özetlemek gerekirse:
★ Basit liste ihtiyaçları için useVirtualizer + sabit estimateSize yeterli.
★ Dinamik yükseklikli içerikler için measureElement callback'ini aktif ediyoruz.
★ Grid yapıları için hem satır hem sütun virtualizer'larını birleştiriyoruz.
★ Infinite scroll için TanStack Query ile entegrasyon temiz bir çözüm sunuyor.
📬 Geri Bildirim
Makaleyi yazarken, kaynakları belirleme, araştırma, yazım denetimi ve araştırma için Claude Opus 4.7 ve GPT 5.4 High modellerini kullandım. Resimleri üretmek için ise Gemini 3.1 Flash Image Preview (Nano Banana 2) ve Gemini 3 Pro Preview 2k (Nano Banana Pro) modellerini kullandım.
Yazı ile ilgili tavsiye, öneri, eleştirileri dikkate alıyorum. İletişime geçmek isterseniz bana websitemdeki sosyal medya adreslerimden veya Linkedin üzerinden ulaşabilirsiniz.
Sevgiyle kalın, Yasin 🤗
📚 Makaleyi Yazarken Kullandığım Kaynaklar
- TanStack Virtual Resmi Dokümantasyonu — API referansı, hook’ların kullanımı ve örnek senaryolar
- TanStack Virtual GitHub Deposu — Örnek uygulamalar ve güncel değişiklikler
- TanStack Query Resmi Dokümantasyonu — Infinite scroll entegrasyonu için
- React Resmi Dokümantasyonu — Rendering — React render döngüsünü anlamak için
- web.dev — Rendering Performance — Tarayıcı render mekanizması ve DOM performansı