Kilka tygodni temu dostałem projekt, w którym strona z listą 8000 produktów zacinała się przy każdym scrollowaniu. Przeglądarka próbowała renderować 8000 DOM nodes, każdy z kilkoma child elementami — na mobile to była porażka. Po 90 minutach pracy z TanStack Virtual scrollowanie było płynne, strona ładowała się szybciej, a DOM zawierał tylko ~20 widocznych elementów.
Wirtualizacja to jedno z tych narzędzi, które kiedyś było nice-to-have, a w 2026 jest must-have dla każdego projektu z list of 500+ elementów. W tym artykule pokazuję, kiedy warto, kiedy paginacja wystarczy, i jak wdrożyć TanStack Virtual krok po kroku.
Kiedy wirtualizacja jest potrzebna
Prosta reguła kciuka: jeśli lista ma więcej niż ~200 widocznych w scroll elementów naraz, rozważ wirtualizację. Dla 1000+ to praktycznie konieczność. Konkretne objawy:
- Scroll zacina się, szczególnie na mobile.
- Mount strony zajmuje zauważalnie długo (sekundy zamiast milisekund).
- React DevTools Profiler pokazuje render pierwszej renderki na 500-2000 ms.
- Każda interakcja (filter, sort) wymusza re-render całej listy — aplikacja „skacze".
- Memory footprint rośnie drastycznie. 10 000 DOM nodes to realnie 50-100 MB RAM.
Wirtualizacja vs paginacja vs infinite scroll
Trzy różne podejścia, trzy różne use case'y.
Paginacja — dzielisz dane na strony po 20–50 elementów, user klika „Następna strona". Dobra dla:
- Tabeli / listy, gdzie user chce widzieć konkretne rekordy (faktury, zamówienia, logi).
- Kiedy URL powinien zachowywać pozycję (shareowalny link).
- Dla SEO, czyli Search Engine Optimization, to optymalizacja strony pod widoczność w wynikach wyszukiwania. — Google indeksuje strony osobno, dobra architektura dla bloga / listingów.
Infinite scroll automatycznie doładowuje kolejne elementy, gdy użytkownik zbliża się do końca listy. — user scrolluje, nowe elementy doładowują się automatycznie. Dobra dla:
- Feed social media, gdzie user nie ma konkretnego celu.
- Eksploracji ofert (marketplace, e-commerce przegląd).
- Gdzie nie ma potrzeby precyzyjnego nawigowania.
Wirtualizacja — renderujesz tylko widoczne elementy, reszta czeka w pamięci. Dobra dla:
- Długich list, gdzie user widzi całość na raz (np. kreator rozpisek w Army Builder z 500 jednostkami).
- Tabel z 1000+ wierszami bez paginacji.
- Chat apps, edytory, data grids.
Wirtualizacja i paginacja NIE są alternatywami — często się łączą. Zaczynasz od strony 20, user scrolluje do końca, ładuje się kolejna (infinite scroll), ale wyrenderowane w DOM jest tylko 20-30 widocznych (wirtualizacja).
Biblioteki — krótko
W 2026 roku trzy sensowne opcje:
1. TanStack Virtual — ten sam zespół co TanStack Query. Headless, framework-agnostic, idealnie typowany, zero DOM decisions. Mój default w nowych projektach.
2. react-window — klasyka, od Brian Vaughn'a (ex-React team). Bardziej opiniated, mniej features, mniejszy bundle. Dobra dla prostych przypadków.
3. Virtuoso — high-level biblioteka z gotowymi komponentami do list, grid, table. Łatwiejsza na start, ale mniej kontroli. Dobra dla zespołów, które chcą „gotowe rozwiązanie".
W tym artykule używam TanStack Virtual, bo jest najelastyczniejsze i współdziała świetnie z TanStack Query.
Instalacja i minimalny setup
Najprostszy przykład — vertical list z fixed-size itemami:
Co się dzieje:
parentRef— referencja do scrollującego się kontenera.useVirtualizer— hook, który na podstawie scroll pozycji oblicza, które itemy są widoczne.virtualizer.getTotalSize()— total wysokość, jaką lista miałaby bez wirtualizacji (dla scrollbar).virtualizer.getVirtualItems()— tylko widoczne (plus overscan) itemy z ich pozycją.transform: translateY(...)— pozycjonowanie każdego widocznego itemu.
Dla 10 000 produktów DOM zawiera tylko ~15 elementów naraz.
Dynamic size — elementy o różnej wysokości
Większość realnych list nie ma fixed-size. Komentarze mają różną długość, produkty mają różne opisy, wiadomości chat mają różne heights. TanStack Virtual obsługuje to przez measureElement:
ref={virtualizer.measureElement} mierzy faktyczną wysokość po render. Za pierwszym przejściem używa estimateSize, potem jest już dokładny.
Horizontal scrolling
Zamień oś przez horizontal: true:
W renderze zamień translateY na translateX i top/height na left/width:
Dla grid (virtualization po obu osiach) TanStack Virtual ma oddzielny useVirtualizer per oś — virtualuje się rows, a wewnątrz każdego row kolejny virtualizer po kolumnach.
Parametry, które decydują o jakości scrolla
TanStack Virtual jest headless, więc jakość UX zależy głównie od konfiguracji:
| Opcja | Znaczenie | Praktyczna zasada |
|---|---|---|
count | Liczba elementów do wirtualizacji | Musi odpowiadać aktualnie przefiltrowanej liście |
getScrollElement | Element, który faktycznie scrolluje | Nie wskazuj window, jeśli scroll jest w panelu |
estimateSize | Szacowany rozmiar elementu przed pomiarem | Dla dynamicznych list przyjmij górny realistyczny zakres |
overscan | Ile elementów renderować poza viewportem | 3-8 zwykle wystarcza; więcej = płynniej, ale ciężej |
measureElement | Pomiar prawdziwego rozmiaru itemu | Używaj przy komentarzach, wiadomościach i kartach o różnej wysokości |
Zbyt mały overscan daje efekt „doczytywania" przy szybkim scrollu. Zbyt duży zabija sens virtualizacji, bo DOM znowu robi się ciężki.
Połączenie z TanStack Query — infinite loading
Najmocniejszy wzorzec — virtualizacja + infinite query. User widzi wyniki natychmiast, nowe ładują się w tle przy scrollowaniu, DOM zawiera tylko widoczne.
User dostaje experience infinite scroll + virtualizację — może scrollować przez 10 000 produktów, DOM zawiera ~15 widocznych, dane są pobierane w kawałkach po 50, cały stack jest zoptymalizowany.
Problemy z wirtualizacją
Wirtualizacja ma swoje koszty i nie każdy case pasuje idealnie.
1. Accessibility
Screen readery czytają DOM. Jeśli user skrolluje listą przez czytnik, może zobaczyć tylko widoczne itemy. Rozwiązania:
aria-setsize="{totalCount}"iaria-posinset="{index + 1}"na każdym itemie.- Keyboard navigation przez jump-to-item (dla długich list).
- Zachowaj właściwe role ARIA (
role="list"na kontenerze,role="listitem"na itemach).
Dla krytycznych accessibility cases (aplikacje rządowe, EAA compliance) rozważ paginację zamiast virtualizacji.
2. CSS z absolute positioning
Virtualne itemy są position: absolute, co może łamać niektóre CSS patterns:
- Sticky headers działają, ale wymagają osobnego wrappera.
- CSS Grid wewnątrz itemu działa, ale grid całej listy — nie.
- Hover effects na sąsiadach — nie zadziałają, bo sąsiedzi nie są tam, gdzie wizualnie są.
3. Ctrl+F / search w przeglądarce
Ctrl+F przeszukuje DOM. Jeśli itemy są poza DOM, nie można ich znaleźć. To realny problem dla user experience. Rozwiązania:
- Własny search input zamiast natywnego Ctrl+F.
- Event listener na keydown z
e.key === 'f' && e.ctrlKey→ focus custom search.
4. Testowanie e2e
Playwright/Cypress nie „widzą" niewidzialnych itemów. Testy muszą najpierw scrollować do elementu, dopiero potem z nim interakcjować. Dla testów to dodatkowa złożoność.
Kiedy NIE używać wirtualizacji
Nie każdy problem „długiej listy" wymaga wirtualizacji. Rozważ prostsze rozwiązania:
1. Paginacja — jeśli lista ma naturalne strony (100 rekordów per strona) i user nawiguje między nimi, paginacja + URL state jest prostsze. Patrz decision tree state management.
2. Filtering zamiast scrollingu — jeśli lista ma 10 000 elementów, ale user zwykle szuka 1-5 konkretnych, filtering (search input) rozwiązuje problem bez virtualizacji.
3. Collapse / hierarchia — drzewo z 10 000 node'ów, gdzie default collapse pokazuje 50, jest wygodniejsze niż flat lista z 10 000.
4. „Pierwsza 100 + Load more" — prosta paginacja z button'em „Pokaż więcej" jest wystarczająca dla 1000-2000 elementów, gdzie większość userów nie dotrze do końca.
Kiedy koniecznie używaj
Są przypadki, gdzie wirtualizacja to nie opcja, tylko wymóg:
- Chat apps — 10 000 wiadomości, user scrolluje w górę szuka kontekstu.
- Data grids (tabele) z 1000+ wierszami, gdzie user chce wszystko widzieć naraz.
- Edytory (code editor, rich text) gdzie pełen dokument to 5000+ linii.
- Calendar view z tysiącami event'ów.
- File browsers dla katalogów z tysiącami plików.
Performance — realny wpływ
Benchmarki z Army Builder — lista 800 jednostek Warhammer:
| Metric | Bez virtualizacji | Z TanStack Virtual |
|---|---|---|
| Initial render | 1800 ms | 120 ms |
| Scroll FPS (mobile) | 15–25 | 55–60 |
| DOM nodes | ~8000 | ~200 |
| Memory (Chrome DevTools) | 85 MB | 18 MB |
Różnice są drastyczne. Dla end usera to różnica między „aplikacja jest frustrująca" a „aplikacja jest responsywna".
Podsumowanie
Wirtualizacja to must-have dla każdej listy z 500+ elementami, szczególnie na mobile. TanStack Virtual w 2026 jest zdecydowanie najlepszym wyborem — elastyczny, świetnie typowany, integruje się bezproblemowo z ekosystem TanStack (Query, Table, Form). Dla mniejszych list, gdzie performance nie jest problemem, paginacja albo filtering są prostsze i często wystarczające.
Jeśli Twoja aplikacja z długimi listami jest wolna — audyt performance pokaże, gdzie wirtualizacja ma sens, a gdzie prostsze rozwiązania wystarczą. W StriveLab React to typowy refactor, który robię regularnie.
