Imagine looking through the observation window of a spaceship at billions of stars in a galaxy. You do not see them all at once - you only see those in your field of view. This is exactly how list virtualization works in React: instead of rendering thousands of DOM elements, we render only those currently visible on the screen.
When we have a list with thousands of elements (e.g., a planet catalog, server logs, chat messages), rendering them all simultaneously can cause serious performance issues:
1// PROBLEM: Rendering 10,000 elements at once
2function SlowStarCatalog({ stars }) {
3 // This will create 10,000 DOM elements at once!
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' }}>Type: {star.type}</span>
15 <span style={{ marginLeft: '10px' }}>Distance: {star.distance} ly</span>
16 </div>
17 ))}
18 </div>
19 );
20}
21
22// Each of the 10,000 elements:
23// - creates DOM nodes
24// - takes up browser memory
25// - slows down initial rendering
26// - slows down scrollingThe effects of this approach are:
Windowing is a technique that renders only elements visible in the "window" (viewport) of the scroll container. The rest of the elements simply do not exist in the DOM - they are added only when the user scrolls the list.
1// CONCEPT: How windowing works
2
3// Imagine a list of 10,000 elements, each 50px tall
4// The container is 500px tall - we see only 10 elements at a time
5
6// WITHOUT windowing:
7// DOM contains: 10,000 elements = 500,000px of height
8// Memory: huge
9
10// WITH windowing:
11// DOM contains: ~15 elements (10 visible + buffer)
12// "Empty" space simulated by padding/transform
13// Memory: minimal
14
15// How it works:
16// [padding-top: simulates elements above]
17// [element 45] <- visible
18// [element 46] <- visible
19// [element 47] <- visible
20// [element 48] <- visible
21// ...
22// [element 54] <- visible
23// [padding-bottom: simulates elements below]react-window is a lightweight library by Brian Vaughn (author of the original react-virtualized) that implements windowing in a simple and efficient way.1import { FixedSizeList } from 'react-window';
2
3// Generate 10,000 stars
4const stars = Array.from({ length: 10000 }, (_, index) => ({
5 id: index,
6 name: `Star-${index + 1}`,
7 type: ['White dwarf', 'Red giant', 'Neutron star', 'Supernova'][index % 4],
8 magnitude: (Math.random() * 20 - 5).toFixed(2)
9}));
10
11// Single row component
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' }}>Star Catalog (10,000 objects)</h2>
37 <FixedSizeList
38 height={400} // Container height
39 width="100%" // Container width
40 itemCount={stars.length} // Number of items
41 itemSize={50} // Height of each item (fixed!)
42 >
43 {StarRow}
44 </FixedSizeList>
45 </div>
46 );
47}FixedSizeList is the most efficient option because it does not need to measure element heights - it knows them in advance. Use it whenever list items have a fixed, known height.When list items have different heights (e.g., chat messages, posts), we use
VariableSizeList:1import { VariableSizeList } from 'react-window';
2
3const messages = Array.from({ length: 5000 }, (_, i) => ({
4 id: i,
5 sender: ['Captain Nova', 'Engineer Rex', 'Navigator Luna'][i % 3],
6 text: i % 5 === 0
7 ? 'Longer message with mission report - status of propulsion systems, communications, and life support. All parameters within normal range.'
8 : 'Short message #' + (i + 1)
9}));
10
11// Function returning element height based on index
12const getItemSize = (index) => {
13 const message = messages[index];
14 // Longer message = taller cell
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}The choice between virtualization and pagination depends on context - just like the choice between a telescope and a star map:
Virtualization is better when:
Pagination is better when:
1// Pagination - classic approach
2function PaginatedList({ totalItems, pageSize }) {
3 const [page, setPage] = useState(1);
4 const totalPages = Math.ceil(totalItems / pageSize);
5
6 return (
7 <div>
8 {/* Render only items from the current page */}
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 Previous
15 </button>
16 <span>Page {page} of {totalPages}</span>
17 <button disabled={page === totalPages} onClick={() => setPage(p => p + 1)}>
18 Next
19 </button>
20 </div>
21 </div>
22 );
23}
24
25// Virtualization - modern approach for long lists
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}To prove that virtualization works, we can measure the performance difference:
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 // Measure render time
9 useEffect(() => {
10 const start = performance.now();
11 // After render
12 requestAnimationFrame(() => {
13 const end = performance.now();
14 console.log(`Render time (${mode}): ${(end - start).toFixed(2)}ms`);
15 });
16 }, [mode]);
17
18 return (
19 <div>
20 <div>
21 <button onClick={() => setMode('normal')}>Normal list</button>
22 <button onClick={() => setMode('virtualized')}>Virtualized</button>
23 </div>
24
25 {mode === 'normal' ? (
26 // 10,000 DOM elements
27 <div style={{ height: 400, overflow: 'auto' }}>
28 {items.map(item => <div key={item.id}>{item.name}</div>)}
29 </div>
30 ) : (
31 // ~15 DOM elements
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// Typical results:
41// Normal list: ~800-2000ms rendering, ~50MB memory
42// Virtualized: ~5-15ms rendering, ~2MB memoryList virtualization is one of the most effective performance optimization techniques in React:
react-window or react-virtuosoFixedSizeList for elements with fixed height (most efficient)VariableSizeList for elements with variable heightperformance.now()Just like a telescope on a spaceship shows you only a part of the sky at any given moment, virtualization shows the user only a part of the list - but with the smoothness worthy of the best navigation systems.