PatternsLists & Performance
Windowing / Virtualization
Render only visible items in the viewport to handle large lists efficiently without blocking the Main Thread.
Windowing / Virtualization
Problem
Rendering thousands of DOM elements causes performance issues: slow initial render, high memory usage, and sluggish scrolling.
Solution
Calculate which items are visible based on scroll position and only render those. This technique is called windowing or virtualization.
interface VirtualListConfig {
totalItems: number;
itemHeight: number;
containerHeight: number;
overscan?: number;
}
interface VirtualListResult {
visibleStartIndex: number;
visibleEndIndex: number;
offsetY: number;
totalHeight: number;
}
function calculateVisibleRange(
scrollTop: number,
config: VirtualListConfig
): VirtualListResult {
const { totalItems, itemHeight, containerHeight, overscan = 3 } = config;
// Calculate visible range
const visibleStartIndex = Math.floor(scrollTop / itemHeight);
const visibleEndIndex = Math.ceil((scrollTop + containerHeight) / itemHeight);
// Add overscan to reduce white flash during fast scroll
const startIndex = Math.max(0, visibleStartIndex - overscan);
const endIndex = Math.min(totalItems, visibleEndIndex + overscan);
// Calculate offset for absolute positioning
const offsetY = startIndex * itemHeight;
const totalHeight = totalItems * itemHeight;
return {
visibleStartIndex: startIndex,
visibleEndIndex: endIndex,
offsetY,
totalHeight,
};
}
// Usage example with 10k items
function setupVirtualList(
container: HTMLElement,
items: unknown[],
itemHeight: number,
renderItem: (item: unknown, index: number) => HTMLElement
): () => void {
const config: VirtualListConfig = {
totalItems: items.length,
itemHeight,
containerHeight: container.clientHeight,
overscan: 5,
};
const innerContainer = document.createElement('div');
innerContainer.style.position = 'relative';
container.appendChild(innerContainer);
const itemsContainer = document.createElement('div');
itemsContainer.style.position = 'absolute';
itemsContainer.style.top = '0';
itemsContainer.style.left = '0';
itemsContainer.style.right = '0';
innerContainer.appendChild(itemsContainer);
function updateVisibleItems(): void {
const scrollTop = container.scrollTop;
const result = calculateVisibleRange(scrollTop, config);
// Update total height
innerContainer.style.height = `${result.totalHeight}px`;
// Update visible items position
itemsContainer.style.transform = `translateY(${result.offsetY}px)`;
// Clear previous items
itemsContainer.innerHTML = '';
// Render only visible items
for (let i = result.visibleStartIndex; i < result.visibleEndIndex; i++) {
const element = renderItem(items[i], i);
itemsContainer.appendChild(element);
}
}
// Initial render
updateVisibleItems();
// Listen to scroll
container.addEventListener('scroll', updateVisibleItems);
// Cleanup function
return () => {
container.removeEventListener('scroll', updateVisibleItems);
};
}Performance Note
With virtualization, a list of 10,000 items only renders 10-20 DOM elements at any time (depending on viewport size). This reduces:
- Initial render time: from seconds to milliseconds
- Memory usage: from hundreds of MB to less than 10MB
- Scroll jank: 60fps smooth scrolling
The overscan parameter prevents white flashes during fast scrolling by pre-rendering a few extra items above and below the visible area.