StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

Doradztwo produktowe

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

Doradztwo produktowe

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

Doradztwo produktowe

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

RealizacjeO mnieBlog
Porozmawiajmy
PL
EN

Nowoczesne strony internetowe dla firm, które myślą odważnie.

Przewiń do góry

Nazwa

StriveLab Maciej Sala

NIP

6772218995

REGON

524008527

E-mail

contact@strivelab.pl

Usługi główne
  • Tworzenie stron internetowych
  • Strony internetowe Next.js
  • Strony internetowe Astro
  • Strony internetowe React
Inne usługi
  • Usługi
  • SEO & Performance Sprint
  • QA & Stabilizacja
  • Konsultacje Product / Delivery
  • Aplikacje webowe Next.js
  • Współpraca ciągła
Strony
  • O mnie
  • Usługi
  • Realizacje
  • Blog

© 2026 StriveLab.pl

Polityka prywatności
ReactPerformanceTanStack Virtual

Wirtualizacja list w React — kiedy TanStack Virtual ratuje FPS, a kiedy wystarczy paginacja

Wirtualizacja list w React — TanStack Virtual, react-window, kiedy stosować, kiedy pagination wystarczy. Dynamic size, horizontal scroll, grid, infinite loading — wzorce z produkcji.

OpublikujLinkedInFacebookWyślij
Autor
Maciej Sala
Opublikowano
13 maja 2026 08:16
Czytanie
7 min czytania
Aktualizacja
Wersja pierwotna

W skrócie

  • Wirtualizacja renderuje tylko widoczne elementy, więc radykalnie zmniejsza liczbę DOM nodes to elementy drzewa dokumentu przeglądarki; zbyt duża ich liczba spowalnia renderowanie i interakcje..
  • Nie zastępuje paginacji; często łączysz paginację, infinite loading i wirtualizację.
  • Dla publicznych stron SEO virtualizacja może ukryć treść przed crawlerem, więc używaj jej głównie w aplikacjach.
  • TanStack Virtual to headless biblioteka do wirtualizacji list i siatek, renderująca tylko widoczny fragment danych. jest dobrym wyborem, gdy potrzebujesz kontroli nad markupiem i integracji z TanStack Query.

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

Code
npm install @tanstack/react-virtual

Najprostszy przykład — vertical list z fixed-size itemami:

Code
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
 
function ProductList({ products }: { products: Product[] }) {
  const parentRef = useRef<HTMLDivElement>(null);
 
  const virtualizer = useVirtualizer({
    count: products.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 80,  // szacowana wysokość itemu w px
    overscan: 5,  // render 5 dodatkowych elementów poza viewport
  });
 
  return (
    <div
      ref={parentRef}
      style={{ height: 600, overflow: 'auto' }}
    >
      <div
        style={{
          height: virtualizer.getTotalSize(),
          width: '100%',
          position: 'relative',
        }}
      >
        {virtualizer.getVirtualItems().map((virtualItem) => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: virtualItem.size,
              transform: `translateY(${virtualItem.start}px)`,
            }}
          >
            <ProductCard product={products[virtualItem.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}

Co się dzieje:

  1. parentRef — referencja do scrollującego się kontenera.
  2. useVirtualizer — hook, który na podstawie scroll pozycji oblicza, które itemy są widoczne.
  3. virtualizer.getTotalSize() — total wysokość, jaką lista miałaby bez wirtualizacji (dla scrollbar).
  4. virtualizer.getVirtualItems() — tylko widoczne (plus overscan) itemy z ich pozycją.
  5. 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:

Code
function Comments({ comments }: { comments: Comment[] }) {
  const parentRef = useRef<HTMLDivElement>(null);
 
  const virtualizer = useVirtualizer({
    count: comments.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 100,  // pierwszy strzał, dokładna wysokość zmierzona przy render
    overscan: 3,
  });
 
  return (
    <div ref={parentRef} style={{ height: 600, overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
        {virtualizer.getVirtualItems().map((virtualItem) => (
          <div
            key={virtualItem.key}
            data-index={virtualItem.index}
            ref={virtualizer.measureElement}  // pomiar przy render
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              transform: `translateY(${virtualItem.start}px)`,
            }}
          >
            <CommentCard comment={comments[virtualItem.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}

ref={virtualizer.measureElement} mierzy faktyczną wysokość po render. Za pierwszym przejściem używa estimateSize, potem jest już dokładny.

Top tip

Przy dynamicznej wysokości lepiej lekko zawyżyć estimateSize, niż zaniżyć. Zaniżony estimate powoduje bardziej widoczne przeskoki scrolla, bo virtualizer musi stale korygować pozycje elementów.

Horizontal scrolling

Zamień oś przez horizontal: true:

Code
const virtualizer = useVirtualizer({
  count: items.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 300,  // szerokość, nie wysokość
  horizontal: true,
  overscan: 5,
});

W renderze zamień translateY na translateX i top/height na left/width:

Code
<div
  style={{
    position: 'absolute',
    top: 0,
    left: 0,
    height: '100%',
    width: virtualItem.size,
    transform: `translateX(${virtualItem.start}px)`,
  }}
>

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:

OpcjaZnaczeniePraktyczna zasada
countLiczba elementów do wirtualizacjiMusi odpowiadać aktualnie przefiltrowanej liście
getScrollElementElement, który faktycznie scrollujeNie wskazuj window, jeśli scroll jest w panelu
estimateSizeSzacowany rozmiar elementu przed pomiaremDla dynamicznych list przyjmij górny realistyczny zakres
overscanIle elementów renderować poza viewportem3-8 zwykle wystarcza; więcej = płynniej, ale ciężej
measureElementPomiar prawdziwego rozmiaru itemuUż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.

Code
import { useInfiniteQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useEffect, useRef } from 'react';
 
function ProductList() {
  const parentRef = useRef<HTMLDivElement>(null);
 
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['products'],
    queryFn: ({ pageParam = 0 }) =>
      fetch(`/api/products?offset=${pageParam}&limit=50`).then(r => r.json()),
    getNextPageParam: (lastPage, pages) =>
      lastPage.hasMore ? pages.length * 50 : undefined,
    initialPageParam: 0,
  });
 
  const allProducts = data?.pages.flatMap(page => page.items) ?? [];
 
  const virtualizer = useVirtualizer({
    count: hasNextPage ? allProducts.length + 1 : allProducts.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 120,
    overscan: 5,
  });
 
  const virtualItems = virtualizer.getVirtualItems();
 
  // Load more when user scrolls to last item
  useEffect(() => {
    const lastItem = virtualItems[virtualItems.length - 1];
    if (!lastItem) return;
 
    if (
      lastItem.index >= allProducts.length - 1 &&
      hasNextPage &&
      !isFetchingNextPage
    ) {
      fetchNextPage();
    }
  }, [virtualItems, hasNextPage, isFetchingNextPage, fetchNextPage, allProducts.length]);
 
  return (
    <div ref={parentRef} style={{ height: 600, overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
        {virtualItems.map((virtualItem) => {
          const isLoaderRow = virtualItem.index > allProducts.length - 1;
          const product = allProducts[virtualItem.index];
 
          return (
            <div
              key={virtualItem.key}
              data-index={virtualItem.index}
              ref={virtualizer.measureElement}
              style={{
                position: 'absolute',
                top: 0,
                left: 0,
                width: '100%',
                transform: `translateY(${virtualItem.start}px)`,
              }}
            >
              {isLoaderRow
                ? (hasNextPage ? <Loader /> : <EndOfList />)
                : <ProductCard product={product} />
              }
            </div>
          );
        })}
      </div>
    </div>
  );
}

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}" i aria-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:

MetricBez virtualizacjiZ TanStack Virtual
Initial render1800 ms120 ms
Scroll FPS (mobile)15–2555–60
DOM nodes~8000~200
Memory (Chrome DevTools)85 MB18 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.

Często zadawane pytania

Pracuję z tym zawodowo.

Jeśli chcesz uporządkować frontend, architekturę React i Next.js, poprawić jakość wdrożenia albo przyspieszyć development bez psucia maintainability, skontaktuj się ze mną. Na co dzień pracuję hands-on przy projektach, w których kod, UX i decyzje produktowe muszą działać razem.

Skontaktuj się ze mną
Maciej Sala

O autorze

Maciej Sala

Maciej Sala — project manager i frontendowiec z doświadczeniem w marketingu internetowym. Na co dzień pracuję z Reactem, Next.js i TypeScriptem, łącząc perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat związany z branżą gier wideo jako project manager i game designer.

Absolwent historii na Uniwersytecie Jagiellońskim i studiów podyplomowych z marketingu internetowego na Akademii Górniczo-Hutniczej w Krakowie. Poza pracą trenuje na siłowni, maluje figurki i realizuje własne projekty.

Moje artykułyWięcej o mnie

Seria

React w praktyce 2026
  1. 1React 19 Actions — formularz bez onSubmit, useOptimistic i useActionState w praktyce
  2. 2React Compiler w 2026 — czy useMemo i useCallback są już martwe?
  3. 3React Query (TanStack) vs SWR vs useEffect — kompletny przewodnik po fetchingu w 2026
  • Kiedy wirtualizacja jest potrzebna1 min
  • Wirtualizacja vs paginacja vs infinite scroll1 min
  • Biblioteki — krótko1 min
  • Instalacja i minimalny setup1 min
  • Dynamic size — elementy o różnej wysokości1 min
  • Horizontal scrolling1 min
  • Parametry, które decydują o jakości scrolla1 min
  • Połączenie z TanStack Query — infinite loading1 min
  • Problemy z wirtualizacją1 min
  • Kiedy NIE używać wirtualizacji1 min
  • Kiedy koniecznie używaj1 min
  • Performance — realny wpływ1 min
  • Podsumowanie1 min

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów
SEO w Astro — Core Web Vitals, dane uporządkowane i techniczny fundament rankingu w 2026
SEO w Astro — Core Web Vitals, dane uporządkowane i techniczny fundament rankingu w 2026

Jak zbudować stronę w Astro, która dominuje w SEO — Core Web Vitals, sitemap, robots.txt, metadane, dane uporządkowane i GEO/AEO. Przewodnik techniczny z konkretnymi implementacjami.

Maciej Sala

Maciej Sala

Founder Strivelab

13 maja 2026
React Query (TanStack) vs SWR vs useEffect — kompletny przewodnik po fetchingu w 2026
React Query (TanStack) vs SWR vs useEffect — kompletny przewodnik po fetchingu w 2026

Jak pobierać dane w React w 2026? Porównanie TanStack Query, SWR i useEffect. Kiedy Server Components wystarczą, kiedy potrzebujesz cache, invalidation, optimistic updates i infinite queries.

Maciej Sala

Maciej Sala

Founder Strivelab

13 maja 2026
React Compiler w 2026 — czy useMemo i useCallback są już martwe?
React Compiler w 2026 — czy useMemo i useCallback są już martwe?

React Compiler stabilny od 2025. Pokazuję, kiedy ręczna memoizacja traci sens, kiedy nadal ma znaczenie i jak realnie wdrożyć Compiler w istniejącym projekcie React.

Maciej Sala

Maciej Sala

Founder Strivelab

13 maja 2026