š
Intermediate
11 min readVirtual Scrolling & Windowing
Rendering large lists efficiently with virtual scrolling
Understanding Virtual Scrolling
Virtual scrolling (windowing) renders only the visible portion of a large list, dramatically improving performance when dealing with thousands or millions of items.
The Problem with Large Lists
// ā Bad: Rendering 10,000 items at once
function LargeList({ items }) {
return (
{items.map(item => (
{item.name}
))}
);
}
// Problems:
// - Creates 10,000 DOM nodes
// - Long initial render time
// - Heavy memory usage
// - Slow scrolling performance
Basic Virtual Scrolling Implementation
function VirtualList({ items, itemHeight = 50, containerHeight = 600 }) {
const [scrollTop, setScrollTop] = useState(0);
// Calculate visible range
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.ceil((scrollTop + containerHeight) / itemHeight);
// Get only visible items
const visibleItems = items.slice(startIndex, endIndex + 1);
// Calculate total height and offset
const totalHeight = items.length * itemHeight;
const offsetY = startIndex * itemHeight;
const handleScroll = (e) => {
setScrollTop(e.target.scrollTop);
};
return (
{visibleItems.map((item, index) => (
{item.name}
))}
);
}
Using react-window
// Install: npm install react-window
import { FixedSizeList } from 'react-window';
// Simple fixed-height list
function MyList({ items }) {
const Row = ({ index, style }) => (
{items[index].name}
);
return (
{Row}
);
}
// Variable height list
import { VariableSizeList } from 'react-window';
function VariableList({ items }) {
const getItemSize = (index) => {
// Return dynamic height based on content
return items[index].content.length > 100 ? 100 : 50;
};
const Row = ({ index, style }) => (
{items[index].title}
{items[index].content}
);
return (
{Row}
);
}
// Grid layout
import { FixedSizeGrid } from 'react-window';
function MyGrid({ items }) {
const Cell = ({ columnIndex, rowIndex, style }) => {
const index = rowIndex * 5 + columnIndex;
return (
{items[index]?.name}
);
};
return (
{Cell}
);
}
Advanced: Dynamic Height with Auto-Sizing
import { VariableSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
function DynamicList({ items }) {
const listRef = useRef();
const rowHeights = useRef({});
// Measure row height after render
const setRowHeight = (index, size) => {
listRef.current.resetAfterIndex(0);
rowHeights.current = { ...rowHeights.current, [index]: size };
};
const getRowHeight = (index) => {
return rowHeights.current[index] || 80; // Default height
};
const Row = ({ index, style }) => {
const rowRef = useRef();
useEffect(() => {
if (rowRef.current) {
setRowHeight(index, rowRef.current.clientHeight);
}
}, [index]);
return (
{items[index].title}
{items[index].content}
);
};
return (
{({ height, width }) => (
{Row}
)}
);
}
Infinite Scrolling with Virtual List
import { FixedSizeList } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
function InfiniteList() {
const [items, setItems] = useState([]);
const [hasMore, setHasMore] = useState(true);
// Check if item is loaded
const isItemLoaded = (index) => !hasMore || index < items.length;
// Load more items
const loadMoreItems = async (startIndex, stopIndex) => {
const newItems = await fetchItems(startIndex, stopIndex);
setItems(prev => [...prev, ...newItems]);
if (newItems.length === 0) {
setHasMore(false);
}
};
const Row = ({ index, style }) => {
const item = items[index];
if (!isItemLoaded(index)) {
return Loading...;
}
return (
{item.name}
);
};
return (
{({ onItemsRendered, ref }) => (
{Row}
)}
);
}
Virtual Scrolling with Intersection Observer
// Lightweight custom implementation
function useVirtualScroll(items, options = {}) {
const {
itemHeight = 50,
overscan = 3, // Extra items to render above/below
containerHeight = 600
} = options;
const [scrollTop, setScrollTop] = useState(0);
const [isScrolling, setIsScrolling] = useState(false);
const scrollTimeoutRef = useRef();
const handleScroll = (e) => {
setScrollTop(e.target.scrollTop);
setIsScrolling(true);
clearTimeout(scrollTimeoutRef.current);
scrollTimeoutRef.current = setTimeout(() => {
setIsScrolling(false);
}, 150);
};
// Calculate visible range with overscan
const startIndex = Math.max(
0,
Math.floor(scrollTop / itemHeight) - overscan
);
const endIndex = Math.min(
items.length - 1,
Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
);
const visibleItems = items.slice(startIndex, endIndex + 1);
const offsetY = startIndex * itemHeight;
const totalHeight = items.length * itemHeight;
return {
visibleItems,
offsetY,
totalHeight,
startIndex,
handleScroll,
isScrolling
};
}
// Usage
function VirtualList({ items }) {
const {
visibleItems,
offsetY,
totalHeight,
startIndex,
handleScroll,
isScrolling
} = useVirtualScroll(items, {
itemHeight: 50,
containerHeight: 600,
overscan: 5
});
return (
{visibleItems.map((item, index) => (
{isScrolling ? (
Loading...
) : (
)}
))}
);
}
Performance Monitoring
// Measure virtual list performance
function usePerformanceMonitor() {
const renderCount = useRef(0);
const startTime = useRef(performance.now());
useEffect(() => {
renderCount.current++;
const elapsed = performance.now() - startTime.current;
if (elapsed > 1000) {
console.log(`FPS: ${(renderCount.current / elapsed * 1000).toFixed(2)}`);
renderCount.current = 0;
startTime.current = performance.now();
}
});
}
// Compare performance
function ComparisonTest() {
const items = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`
}));
// Regular list (slow)
const RegularList = () => (
{items.map(item => (
{item.name}
))}
);
// Virtual list (fast)
const VirtualList = () => (
{({ index, style }) => (
{items[index].name}
)}
);
}
Best Practices
- Use virtual scrolling for lists with 100+ items
- Add overscan (extra rendered items) for smoother scrolling
- Use
react-windowfor simple cases,react-virtualizedfor complex ones - Implement dynamic height calculation for variable content
- Combine with infinite scrolling for large datasets
- Use
will-change: transformCSS for smoother scrolling - Avoid heavy computations in row components
- Memoize row components with
React.memo - Test performance with realistic data volumes
- Consider grid virtualization for table-like layouts