Front-end Engineering Lab

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.

On this page