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.
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 scrollowanieEfekty takiego podejscia to:
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 to lekka biblioteka autorstwa Briana Vaughna (autora oryginalnego react-virtualized), ktora implementuje windowing w sposob prosty i wydajny.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.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}Wybor miedzy wirtualizacja a paginacja zalezy od kontekstu - tak jak wybor miedzy teleskopem a mapa gwiazd:
Wirtualizacja jest lepsza gdy:
Paginacja jest lepsza gdy:
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}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 pamieciWirtualizacja list to jedna z najskuteczniejszych technik optymalizacji wydajnosci w React:
react-window lub react-virtuosoFixedSizeList dla elementow o stalej wysokosci (najwydajniejsze)VariableSizeList dla elementow o zmiennej wysokosciperformance.now()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.