Virtualization — react-virtual, katta ro'yxatlar
Muammodan boshlaylik
Admin panelda buyurtmalar jadvali — 10 000 ta qator:
function Buyurtmalar({ buyurtmalar }) {
return (
<ul>
{buyurtmalar.map((b) => <BuyurtmaQatori key={b.id} buyurtma={b} />)}
</ul>
);
}
Natija:
- Sahifa ochilishi 3–4 soniya — brauzer 10 000 ta
<li>va ularning ichidagi o'n minglab elementlarni yaratadi - Aylantirish (scroll) "qotib-qotib" ketadi
- Qidiruvda har bir harf — 10 000 qatorni qayta render
memo(68-dars) yordam bermaydi: muammo qayta render da emas, DOM elementlarining sonida
Ekranda bir vaqtda faqat ~15 ta qator ko'rinadi. Qolgan 9 985 tasi — ekrandan tashqarida, lekin xotirani band qilib, brauzerni sekinlashtirib turibdi.
G'oya
Virtualizatsiya (windowing) — faqat ko'rinib turgan elementlarni render qilish:
┌─────────────────────────┐
│ (bo'sh joy: 4000px) │ ← 0–99 qatorlar: DOM da YO'Q, faqat balandlik
├─────────────────────────┤
│ 100. Buyurtma #10100 │ ┐
│ 101. Buyurtma #10101 │ │
│ 102. Buyurtma #10102 │ │ ← faqat shular DOM da (~15 ta + zaxira)
│ ... │ │
│ 114. Buyurtma #10114 │ ┘
├─────────────────────────┤
│ (bo'sh joy: 395000px) │ ← 115–9999: DOM da YO'Q
└─────────────────────────┘
- Konteyner butun ro'yxat balandligiga ega — scroll bar to'g'ri ishlaydi
- Faqat ko'rinadigan qatorlar (+ bir nechta zaxira) DOM da,
position: absolutebilan o'z joyida - Scroll qilinganda — yuqoridagilar o'chiriladi, pastdagilar qo'shiladi
10 000 o'rniga ~20 ta element. Ro'yxat 100 000 ta bo'lsa ham — baribir ~20 ta.
TanStack Virtual
npm install @tanstack/react-virtual
TanStack Query bilan bir oiladan; "headless" — faqat hisob-kitob qiladi, HTML va stillarni o'zingiz yozasiz.
import { useRef } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
function Buyurtmalar({ buyurtmalar }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: buyurtmalar.length, // jami elementlar
getScrollElement: () => parentRef.current,
estimateSize: () => 56, // bitta qator balandligi (px)
overscan: 5, // ko'rinmaydigan zaxira qatorlar
});
return (
<div
ref={parentRef}
style={{ height: '600px', overflow: 'auto' }} // scroll konteyner
>
<div
style={{
height: `${virtualizer.getTotalSize()}px`, // butun ro'yxat balandligi
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((item) => {
const b = buyurtmalar[item.index];
return (
<div
key={item.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${item.size}px`,
transform: `translateY(${item.start}px)`,
}}
>
#{b.raqam} — {b.mijoz} — {b.summa.toLocaleString()} so'm
</div>
);
})}
</div>
</div>
);
}
Uch qatlam:
| Element | Vazifasi |
|---|---|
Tashqi div (parentRef) |
Scroll konteyner — aniq balandlik va overflow: auto |
O'rta div |
Butun ro'yxat balandligi — scroll bar uchun |
| Ichki elementlar | Faqat ko'rinadiganlar, translateY(item.start) bilan joyida |
getVirtualItems() har bir element uchun beradi: index (ma'lumotdagi o'rni), start (yuqoridan masofa), size, key.
[!WARNING] Scroll konteynerga aniq balandlik bering (
height: 600px,height: 100vhyoki flex/grid orqali cheklangan balandlik). Balandlik bo'lmasa konteyner ro'yxat bilan birga o'sadi, scroll bo'lmaydi, va virtualizer hamma elementlarni "ko'rinadi" deb hisoblaydi — virtualizatsiya ishlamaydi.
Har xil balandlikdagi elementlar
Chat xabarlari, izohlar — har birining balandligi har xil. estimateSize — faqat taxmin; haqiqiy o'lchamni o'lchash uchun measureElement:
const virtualizer = useVirtualizer({
count: xabarlar.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 80, // o'rtacha taxmin
});
{virtualizer.getVirtualItems().map((item) => (
<div
key={item.key}
data-index={item.index} // majburiy — qaysi element o'lchanayotganini bilish uchun
ref={virtualizer.measureElement} // haqiqiy balandlikni o'lchaydi
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${item.start}px)`,
// height yo'q — kontentga qarab
}}
>
<Xabar xabar={xabarlar[item.index]} />
</div>
))}
Element render bo'lgandan keyin virtualizer uning haqiqiy balandligini o'lchaydi va qolgan elementlarning joylashuvini tuzatadi. Taxmin haqiqatga qanchalik yaqin bo'lsa — scroll shunchalik silliq.
Butun sahifa scroll i bilan
Alohida konteyner o'rniga brauzer oynasining o'zi scroll qilinsa:
import { useWindowVirtualizer } from '@tanstack/react-virtual';
const virtualizer = useWindowVirtualizer({
count: mahsulotlar.length,
estimateSize: () => 120,
scrollMargin: listRef.current?.offsetTop ?? 0, // ro'yxat sahifaning qayerdan boshlanadi
});
Lenta, katalog kabi sahifalar uchun.
Grid — gorizontal ham
useVirtualizer ning horizontal: true parametri va ikkita virtualizer (qatorlar va ustunlar) bilan katta jadvallar va rasm galereyalarini ham virtualizatsiya qilish mumkin.
Cheksiz scroll bilan
useInfiniteQuery (54-dars) + virtualizatsiya:
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({ ... });
const hammasi = data ? data.pages.flatMap((p) => p.items) : [];
const virtualizer = useVirtualizer({
count: hasNextPage ? hammasi.length + 1 : hammasi.length, // +1 — "yuklanmoqda" qatori
getScrollElement: () => parentRef.current,
estimateSize: () => 100,
});
const items = virtualizer.getVirtualItems();
useEffect(() => {
const oxirgi = items.at(-1);
if (oxirgi && oxirgi.index >= hammasi.length - 1 && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [items, hammasi.length, hasNextPage, isFetchingNextPage, fetchNextPage]);
Ro'yxat oxiriga yaqinlashganda keyingi sahifa yuklanadi, DOM da esa baribir ~20 ta element.
Kamchiliklari
| Muammo | Tushuntirish |
|---|---|
| Ctrl+F ishlamaydi | Brauzer qidiruvi faqat DOM dagi matnni topadi — ko'rinmaydigan qatorlar DOM da yo'q |
| Accessibility | Ekran o'quvchi ro'yxat hajmini bilmaydi; aria-rowcount, aria-rowindex (jadvallarda) yoki aria-setsize, aria-posinset bilan yordam bering |
| Murakkablik | Absolute pozitsiya, o'lchash, scroll konteyner — oddiy map dan ancha murakkab |
| Tez scroll | Juda tez aylantirilganda bir zum bo'sh joy ko'rinishi mumkin — overscan ni oshiring |
| Sahifani chop etish | Faqat ko'rinadigan qism chop etiladi |
[!TIP] Virtualizatsiyadan oldin oddiyroq yechimlarni o'ylang: sahifalash (54-dars) — 10 000 qatorni bir vaqtda ko'rsatish shartmi? Yoki CSS ning
content-visibility: autoxususiyati — brauzer ekrandan tashqaridagi elementlarni chizmaydi, lekin ular DOM da qoladi (Ctrl+F ishlaydi). Bir necha yuzlab element uchun ko'pincha shuning o'zi yetarli. Virtualizatsiya — minglab elementlar uchun.
Nima uchun shunday?
Nima uchun ko'p DOM elementlari sahifani sekinlashtiradi — React faqat o'zgarganlarini yangilaydi-ku?
React ning yangilashi samarali, lekin muammo React da emas — brauzerda. Har bir DOM elementi xotira egallaydi va har bir o'zgarishda brauzer stillarni hisoblaydi (style), joylashuvni hisoblaydi (layout) va chizadi (paint). Bu hisob-kitoblar elementlar soniga bog'liq: 100 000 element bilan scroll, resize yoki bitta element balandligining o'zgarishi ham butun sahifani qayta hisoblashga olib kelishi mumkin. Virtualizatsiya muammoni manbadan hal qiladi: brauzer faqat foydalanuvchi haqiqatan ko'rayotgan narsani boshqaradi. Bu o'yinlar dunyosidagi "culling" g'oyasiga o'xshaydi — kameradan tashqaridagi obyektlar chizilmaydi.
Xulosa
Eslab qoling:
- Muammo — DOM elementlari soni;
memoyordam bermaydi - Virtualizatsiya — faqat ko'rinadigan elementlar + zaxira DOM da; qolgani — faqat balandlik
@tanstack/react-virtual:useVirtualizer({ count, getScrollElement, estimateSize, overscan })getTotalSize()— ichki konteyner balandligi;getVirtualItems()—index,start,size,key- Scroll konteynerga aniq balandlik
- Har xil balandlik —
measureElement+data-index useWindowVirtualizer— sahifa scroll i;useInfiniteQuerybilan birga ishlaydi- Kamchiliklar: Ctrl+F, accessibility, murakkablik; avval sahifalash yoki
content-visibilityni o'ylang
Keyingi dars
71-dars: React Profiler — sekin componentlarni topish