SalakCode SalakCode
Performance

Virtual Scrolling

Render only visible items in large lists

Advanced
performance lists dom rendering

Definition

Virtual scrolling is a technique that renders only the items visible in the viewport, plus a small buffer above and below. This allows smooth scrolling through lists of any size (thousands or millions of items) while keeping DOM nodes and memory usage minimal.

How It Works

Viewport: 500px height
Item height: 50px
Total items: 10,000

Without virtualization:
- DOM nodes: 10,000
- Memory: Very high
- Performance: Poor

With virtualization:
- Visible items: 10 (500px / 50px)
- Buffer: 5 above + 5 below
- DOM nodes: 20
- Memory: Minimal
- Performance: Smooth

Basic Implementation

Vanilla JavaScript

class VirtualScroller {
  constructor(container, options) {
    this.container = container;
    this.itemHeight = options.itemHeight;
    this.totalItems = options.totalItems;
    this.renderItem = options.renderItem;
    
    this.visibleCount = Math.ceil(container.clientHeight / this.itemHeight);
    this.buffer = 5;
    
    this.init();
  }
  
  init() {
    // Create scroll container
    this.scrollContainer = document.createElement('div');
    this.scrollContainer.style.height = 
      `${this.totalItems * this.itemHeight}px`;
    this.scrollContainer.style.position = 'relative';
    
    // Create visible items container
    this.visibleContainer = document.createElement('div');
    this.visibleContainer.style.position = 'absolute';
    this.visibleContainer.style.top = '0';
    this.visibleContainer.style.left = '0';
    this.visibleContainer.style.right = '0';
    
    this.scrollContainer.appendChild(this.visibleContainer);
    this.container.appendChild(this.scrollContainer);
    
    // Initial render
    this.renderVisibleItems();
    
    // Listen to scroll
    this.container.addEventListener('scroll', () => {
      requestAnimationFrame(() => this.renderVisibleItems());
    });
  }
  
  renderVisibleItems() {
    const scrollTop = this.container.scrollTop;
    const startIndex = Math.floor(scrollTop / this.itemHeight);
    const visibleStart = Math.max(0, startIndex - this.buffer);
    const visibleEnd = Math.min(
      this.totalItems,
      startIndex + this.visibleCount + this.buffer
    );
    
    // Clear current items
    this.visibleContainer.innerHTML = '';
    
    // Render visible items
    for (let i = visibleStart; i < visibleEnd; i++) {
      const item = this.renderItem(i);
      item.style.position = 'absolute';
      item.style.top = `${i * this.itemHeight}px`;
      item.style.height = `${this.itemHeight}px`;
      item.style.left = '0';
      item.style.right = '0';
      this.visibleContainer.appendChild(item);
    }
    
    // Adjust container position
    this.visibleContainer.style.transform = 
      `translateY(${visibleStart * this.itemHeight}px)`;
  }
}

// Usage
const scroller = new VirtualScroller(
  document.getElementById('list'),
  {
    itemHeight: 50,
    totalItems: 10000,
    renderItem: (index) => {
      const div = document.createElement('div');
      div.textContent = `Item ${index}`;
      div.className = 'list-item';
      return div;
    }
  }
);

React Implementation

Using react-window

import { FixedSizeList as List } from 'react-window';

function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style} className="list-item">
      {items[index].name}
    </div>
  );
  
  return (
    <List
      height={500}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </List>
  );
}

Custom React Hook

import { useState, useEffect, useRef, useCallback } from 'react';

function useVirtualList(itemCount, itemHeight, overscan = 5) {
  const containerRef = useRef(null);
  const [scrollTop, setScrollTop] = useState(0);
  
  const visibleRange = useCallback(() => {
    const containerHeight = containerRef.current?.clientHeight || 0;
    const start = Math.floor(scrollTop / itemHeight);
    const visibleCount = Math.ceil(containerHeight / itemHeight);
    
    return {
      start: Math.max(0, start - overscan),
      end: Math.min(itemCount, start + visibleCount + overscan)
    };
  }, [scrollTop, itemHeight, itemCount, overscan]);
  
  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;
    
    const handleScroll = () => {
      setScrollTop(container.scrollTop);
    };
    
    container.addEventListener('scroll', handleScroll);
    return () => container.removeEventListener('scroll', handleScroll);
  }, []);
  
  const { start, end } = visibleRange();
  const totalHeight = itemCount * itemHeight;
  const offsetY = start * itemHeight;
  
  return {
    containerRef,
    virtualItems: { start, end },
    totalHeight,
    offsetY
  };
}

// Usage
function MyVirtualList({ items }) {
  const { containerRef, virtualItems, totalHeight, offsetY } = 
    useVirtualList(items.length, 50);
  
  const visibleItems = items.slice(virtualItems.start, virtualItems.end);
  
  return (
    <div ref={containerRef} style={{ height: '500px', overflow: 'auto' }}>
      <div style={{ height: totalHeight, position: 'relative' }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map((item, index) => (
            <div key={item.id} style={{ height: 50 }}>
              {item.name}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

Advanced Features

Variable Height Items

// Measure items dynamically
function VariableHeightList({ items }) {
  const [heights, setHeights] = useState({});
  const itemRefs = useRef({});
  
  useEffect(() => {
    // Measure actual heights after render
    const newHeights = {};
    Object.keys(itemRefs.current).forEach(key => {
      const el = itemRefs.current[key];
      if (el) {
        newHeights[key] = el.getBoundingClientRect().height;
      }
    });
    setHeights(newHeights);
  }, [items]);
  
  // Calculate positions based on heights
  const getItemOffset = (index) => {
    let offset = 0;
    for (let i = 0; i < index; i++) {
      offset += heights[i] || estimatedHeight;
    }
    return offset;
  };
  
  // ... render logic
}

Dynamic Loading

function InfiniteVirtualList() {
  const [items, setItems] = useState([]);
  const [hasMore, setHasMore] = useState(true);
  const loaderRef = useRef(null);
  
  const loadMore = useCallback(async () => {
    if (!hasMore) return;
    
    const newItems = await fetchItems(items.length, 20);
    setItems(prev => [...prev, ...newItems]);
    setHasMore(newItems.length === 20);
  }, [items.length, hasMore]);
  
  useEffect(() => {
    const observer = new IntersectionObserver(
      entries => {
        if (entries[0].isIntersecting) {
          loadMore();
        }
      },
      { threshold: 0 }
    );
    
    if (loaderRef.current) {
      observer.observe(loaderRef.current);
    }
    
    return () => observer.disconnect();
  }, [loadMore]);
  
  return (
    <VirtualList items={items} />
    {hasMore && <div ref={loaderRef}>Loading...</div>}
  );
}

Performance Tips

// 1. Use requestAnimationFrame for scroll handling
container.addEventListener('scroll', () => {
  requestAnimationFrame(() => renderVisibleItems());
});

// 2. Debounce resize handlers
const debouncedResize = debounce(handleResize, 100);
window.addEventListener('resize', debouncedResize);

// 3. Use CSS containment
.list-container {
  contain: strict;
}

// 4. Recycle DOM elements instead of creating new ones
class Recycler {
  constructor(createFn) {
    this.createFn = createFn;
    this.pool = [];
  }
  
  get() {
    return this.pool.pop() || this.createFn();
  }
  
  recycle(element) {
    this.pool.push(element);
  }
}
Key Takeaway

Virtual scrolling enables rendering massive lists by only keeping visible items in the DOM. Use libraries like react-window for React, or implement custom solutions using absolute positioning and scroll tracking. Handle variable heights with dynamic measurement, and always include buffer zones for smooth scrolling.

Resources

Related Topics