Virtualization — react-virtual, katta ro'yxatlar

~20 daq · React · Performance

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: absolute bilan 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: 100vh yoki 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: auto xususiyati — 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; memo yordam 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; useInfiniteQuery bilan birga ishlaydi
  • Kamchiliklar: Ctrl+F, accessibility, murakkablik; avval sahifalash yoki content-visibility ni o'ylang

Keyingi dars

71-dars: React Profiler — sekin componentlarni topish