Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Wirtualizacja list i windowing

Wyobraz sobie, ze patrzysz przez okno obserwacyjne statku kosmicznego na miliardy gwiazd w galaktyce. Nie widzisz wszystkich naraz - widzisz tylko te, ktore sa w twoim polu widzenia. Wlasnie na tym polega wirtualizacja list w React: zamiast renderowac tysiace elementow DOM, renderujemy tylko te, ktore sa aktualnie widoczne na ekranie.

Problem: renderowanie tysiecy elementow

Gdy mamy liste z tysiacami elementow (np. lista planet, logi z serwera, wiadomosci czatu), renderowanie ich wszystkich jednoczesnie moze spowodowac powaznie problemy z wydajnoscia:

1// PROBLEM: Renderowanie 10 000 elementow na raz
2function SlowStarCatalog({ stars }) {
3  // To stworzy 10 000 elementow DOM na raz!
4  return (
5    <div style={{ height: '600px', overflow: 'auto' }}>
6      {stars.map(star => (
7        <div key={star.id} style={{
8          padding: '12px',
9          borderBottom: '1px solid #333',
10          backgroundColor: '#0a0a1a',
11          color: '#ccc'
12        }}>
13          <strong style={{ color: '#ffd700' }}>{star.name}</strong>
14          <span style={{ marginLeft: '10px' }}>Typ: {star.type}</span>
15          <span style={{ marginLeft: '10px' }}>Odleglosc: {star.distance} ly</span>
16        </div>
17      ))}
18    </div>
19  );
20}
21
22// Kazdy z 10 000 elementow:
23// - tworzy wezly DOM
24// - zajmuje pamiec przegladarki
25// - spowalnia poczatkowe renderowanie
26// - spowalnia scrollowanie

Efekty takiego podejscia to:

  • Dlugi czas poczatkowego renderowania - przegladarka musi stworzyc tysiecy elementow DOM
  • Wysokie zuzycie pamieci - kazdy element DOM zajmuje pamiec
  • Lagujace scrollowanie - przegladarka musi obslugiwac ogromne drzewo DOM
  • Zamrazanie interfejsu - uzytkownik czeka na odpowiedz aplikacji

Koncept windowing

Windowing (okienkowanie) to technika, ktora renderuje tylko elementy widoczne w "oknie" (viewport) kontenera scroll. Reszta elementow po prostu nie istnieje w DOM - sa dodawane dopiero gdy uzytkownik przewija liste.

1// KONCEPT: Jak dziala windowing
2
3// Wyobraz sobie liste 10 000 elementow, kazdy o wysokosci 50px
4// Kontener ma wysokosc 500px - widzimy tylko 10 elementow naraz
5
6// BEZ windowing:
7// DOM zawiera: 10 000 elementow = 500 000px wysokosci
8// Pamiec: ogromna
9
10// Z windowing:
11// DOM zawiera: ~15 elementow (10 widocznych + bufor)
12// "Pusta" przestrzen symulowana przez padding/transform
13// Pamiec: minimalna
14
15// Schemat dzialania:
16// [padding-top: symuluje elementy powyzej]
17// [element 45] <- widoczny
18// [element 46] <- widoczny
19// [element 47] <- widoczny
20// [element 48] <- widoczny
21// ...
22// [element 54] <- widoczny
23// [padding-bottom: symuluje elementy ponizej]

react-window - biblioteka do wirtualizacji

react-window
to lekka biblioteka autorstwa Briana Vaughna (autora oryginalnego
react-virtualized
), ktora implementuje windowing w sposob prosty i wydajny.

FixedSizeList - elementy o stalej wysokosci

1import { FixedSizeList } from 'react-window';
2
3// Generujemy 10 000 gwiazd
4const stars = Array.from({ length: 10000 }, (_, index) => ({
5  id: index,
6  name: `Gwiazda-${index + 1}`,
7  type: ['Karzel bialy', 'Olbrzym czerwony', 'Gwiazda neutronowa', 'Supernowa'][index % 4],
8  magnitude: (Math.random() * 20 - 5).toFixed(2)
9}));
10
11// Komponent pojedynczego wiersza
12function StarRow({ index, style }) {
13  const star = stars[index];
14  return (
15    <div style={{
16      ...style,
17      display: 'flex',
18      alignItems: 'center',
19      padding: '0 16px',
20      borderBottom: '1px solid #1a1a3e',
21      backgroundColor: index % 2 === 0 ? '#0a0a1a' : '#0f0f2a',
22      color: '#ccc'
23    }}>
24      <span style={{ flex: 1, color: '#ffd700' }}>{star.name}</span>
25      <span style={{ flex: 1 }}>{star.type}</span>
26      <span style={{ width: '100px', textAlign: 'right' }}>
27        mag: {star.magnitude}
28      </span>
29    </div>
30  );
31}
32
33function VirtualizedStarCatalog() {
34  return (
35    <div style={{ padding: '20px', backgroundColor: '#0f0f23' }}>
36      <h2 style={{ color: '#00d2ff' }}>Katalog Gwiazd (10 000 obiektow)</h2>
37      <FixedSizeList
38        height={400}         // Wysokosc kontenera
39        width="100%"         // Szerokosc kontenera
40        itemCount={stars.length}  // Liczba elementow
41        itemSize={50}        // Wysokosc kazdego elementu (stala!)
42      >
43        {StarRow}
44      </FixedSizeList>
45    </div>
46  );
47}

FixedSizeList
jest najwydajniejsza opcja, poniewaz nie musi mierzyc wysokosci elementow - zna ja z gory. Uzyj jej zawsze, gdy elementy listy maja stala, znana wysokosc.

VariableSizeList - elementy o roznej wysokosci

Gdy elementy listy maja rozne wysokosci (np. wiadomosci czatu, posty), uzywamy

VariableSizeList
:

1import { VariableSizeList } from 'react-window';
2
3const messages = Array.from({ length: 5000 }, (_, i) => ({
4  id: i,
5  sender: ['Kapitan Nova', 'Inzynier Rex', 'Nawigator Luna'][i % 3],
6  text: i % 5 === 0
7    ? 'Dluzsza wiadomosc z raportem misji - status systemow napaedowych, komunikacji i podtrzymywania zycia. Wszystkie parametry w normie.'
8    : 'Krotka wiadomosc #' + (i + 1)
9}));
10
11// Funkcja zwracajaca wysokosc elementu na podstawie indeksu
12const getItemSize = (index) => {
13  const message = messages[index];
14  // Dluzsza wiadomosc = wyzsza komorka
15  return message.text.length > 50 ? 80 : 50;
16};
17
18function MessageRow({ index, style }) {
19  const msg = messages[index];
20  return (
21    <div style={{
22      ...style,
23      padding: '8px 16px',
24      borderBottom: '1px solid #1a1a3e',
25      backgroundColor: '#0a0a1a',
26    }}>
27      <strong style={{ color: '#e94560' }}>{msg.sender}:</strong>
28      <p style={{ color: '#ccc', margin: '4px 0 0 0', fontSize: '14px' }}>
29        {msg.text}
30      </p>
31    </div>
32  );
33}
34
35function VirtualizedChat() {
36  return (
37    <VariableSizeList
38      height={400}
39      width="100%"
40      itemCount={messages.length}
41      itemSize={getItemSize}
42    >
43      {MessageRow}
44    </VariableSizeList>
45  );
46}

Kiedy uzywac wirtualizacji vs paginacji?

Wybor miedzy wirtualizacja a paginacja zalezy od kontekstu - tak jak wybor miedzy teleskopem a mapa gwiazd:

Wirtualizacja jest lepsza gdy:

  • Uzytkownik potrzebuje plynnego scrollowania (np. feed, czat, logi)
  • Dane sa juz zaladowane (lub ladowane strumieniowo)
  • Zachowanie "naturalnego" scrollbara jest wazne
  • Lista jest bardzo dluga, ale elementy sa proste

Paginacja jest lepsza gdy:

  • Dane sa ladowane z serwera stronicami
  • Uzytkownik chce przeskakiwac do konkretnych stron
  • SEO jest wazne (kazda strona ma osobny URL)
  • Elementy listy sa bardzo zlozone
1// Paginacja - klasyczne podejscie
2function PaginatedList({ totalItems, pageSize }) {
3  const [page, setPage] = useState(1);
4  const totalPages = Math.ceil(totalItems / pageSize);
5
6  return (
7    <div>
8      {/* Renderuj tylko elementy aktualnej strony */}
9      {items.slice((page - 1) * pageSize, page * pageSize).map(item => (
10        <ItemCard key={item.id} item={item} />
11      ))}
12      <div>
13        <button disabled={page === 1} onClick={() => setPage(p => p - 1)}>
14          Poprzednia
15        </button>
16        <span>Strona {page} z {totalPages}</span>
17        <button disabled={page === totalPages} onClick={() => setPage(p => p + 1)}>
18          Nastepna
19        </button>
20      </div>
21    </div>
22  );
23}
24
25// Wirtualizacja - nowoczesne podejscie dla dlugich list
26function VirtualizedList({ items }) {
27  return (
28    <FixedSizeList
29      height={600}
30      itemCount={items.length}
31      itemSize={60}
32    >
33      {({ index, style }) => (
34        <div style={style}>
35          <ItemCard item={items[index]} />
36        </div>
37      )}
38    </FixedSizeList>
39  );
40}

Mierzenie poprawy wydajnosci

Aby udowodnic, ze wirtualizacja dziala, mozemy zmierzyc roznice w wydajnosci:

1function PerformanceComparison() {
2  const [mode, setMode] = useState('virtualized');
3  const items = useMemo(
4    () => Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `Item ${i}` })),
5    []
6  );
7
8  // Mierzymy czas renderowania
9  useEffect(() => {
10    const start = performance.now();
11    // Po renderze
12    requestAnimationFrame(() => {
13      const end = performance.now();
14      console.log(`Czas renderowania (${mode}): ${(end - start).toFixed(2)}ms`);
15    });
16  }, [mode]);
17
18  return (
19    <div>
20      <div>
21        <button onClick={() => setMode('normal')}>Normalna lista</button>
22        <button onClick={() => setMode('virtualized')}>Zwirtualizowana</button>
23      </div>
24
25      {mode === 'normal' ? (
26        // 10 000 elementow DOM
27        <div style={{ height: 400, overflow: 'auto' }}>
28          {items.map(item => <div key={item.id}>{item.name}</div>)}
29        </div>
30      ) : (
31        // ~15 elementow DOM
32        <FixedSizeList height={400} itemCount={items.length} itemSize={35}>
33          {({ index, style }) => <div style={style}>{items[index].name}</div>}
34        </FixedSizeList>
35      )}
36    </div>
37  );
38}
39
40// Typowe wyniki:
41// Normalna lista: ~800-2000ms renderowania, ~50MB pamieci
42// Zwirtualizowana: ~5-15ms renderowania, ~2MB pamieci

Podsumowanie

Wirtualizacja list to jedna z najskuteczniejszych technik optymalizacji wydajnosci w React:

  1. Renderuj tylko widoczne elementy - uzyj
    react-window
    lub
    react-virtuoso
  2. FixedSizeList
    dla elementow o stalej wysokosci (najwydajniejsze)
  3. VariableSizeList
    dla elementow o zmiennej wysokosci
  4. Mierz przed i po - uzywaj React DevTools Profiler i
    performance.now()
  5. Rozważ paginacje jako alternatywe, gdy dane sa ladowane z serwera

Tak jak teleskop na statku kosmicznym pokazuje nam tylko fragment nieba w danym momencie, wirtualizacja pokazuje uzytkownikowi tylko fragment listy - ale z plynnoscia godna najlepszych systemow nawigacyjnych.

Vai a CodeWorlds