Rendering large lists without virtualization causing slow UI

Loading

Optimizing Large List Rendering in React

Rendering large lists without virtualization is a common performance pitfall that can lead to sluggish user interfaces, especially when dealing with hundreds or thousands of items.

The Problem: Unoptimized List Rendering

// ❌ Performance-heavy approach
function BigList({ items }) {
  return (
    <ul>
      {items.map(item => (
        <ListItem key={item.id} item={item} />
      ))}
    </ul>
  );
}

Why This is Problematic

  1. DOM Overload: All list items are rendered in the DOM simultaneously
  2. Memory Usage: High memory consumption from many component instances
  3. Slow Scrolling: Browser struggles with layout and repaints
  4. Wasted Renders: Components off-screen still consume resources

Solution: List Virtualization

1. Using React Window (Recommended)

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

function VirtualizedList({ items }) {
  return (
    <List
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>
          <ListItem item={items[index]} />
        </div>
      )}
    </List>
  );
}

2. With Dynamic Item Sizes (react-window)

import { VariableSizeList } from 'react-window';

const rowHeights = new Array(1000)
  .fill(true)
  .map(() => 25 + Math.round(Math.random() * 50));

function DynamicList({ items }) {
  return (
    <VariableSizeList
      height={600}
      itemCount={items.length}
      itemSize={index => rowHeights[index]}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>
          <ListItem item={items[index]} />
        </div>
      )}
    </VariableSizeList>
  );
}

3. Horizontal Lists

import { FixedSizeList } from 'react-window';

function HorizontalList({ items }) {
  return (
    <FixedSizeList
      height={150}
      itemCount={items.length}
      itemSize={200}
      layout="horizontal"
      width={600}
    >
      {({ index, style }) => (
        <div style={style}>
          <ListItem item={items[index]} />
        </div>
      )}
    </FixedSizeList>
  );
}

Alternative Solutions

1. Pagination

function PaginatedList({ items }) {
  const [page, setPage] = useState(1);
  const itemsPerPage = 50;

  const paginatedItems = items.slice(
    (page - 1) * itemsPerPage,
    page * itemsPerPage
  );

  return (
    <div>
      <ul>
        {paginatedItems.map(item => (
          <ListItem key={item.id} item={item} />
        ))}
      </ul>
      <Pagination
        currentPage={page}
        totalItems={items.length}
        itemsPerPage={itemsPerPage}
        onPageChange={setPage}
      />
    </div>
  );
}

2. Infinite Loading

import { useInfiniteQuery } from 'react-query';

function InfiniteList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery(
    'items',
    ({ pageParam = 0 }) => fetchItems(pageParam),
    {
      getNextPageParam: (lastPage) => lastPage.nextPage,
    }
  );

  return (
    <ul>
      {data.pages.map(page => (
        page.items.map(item => (
          <ListItem key={item.id} item={item} />
        ))
      ))}
      {hasNextPage && (
        <button 
          onClick={() => fetchNextPage()}
          disabled={isFetchingNextPage}
        >
          {isFetchingNextPage ? 'Loading...' : 'Load More'}
        </button>
      )}
    </ul>
  );
}

Performance Optimization Tips

  1. Stable Keys: Ensure list items have unique, stable keys
  2. Memoize Components: Use React.memo for list items
  3. Avoid Inline Styles/Functions: In item renderers
  4. Lazy Loading: For images/media within list items
  5. CSS Containment: Use contain: strict where possible

Common Pitfalls

  1. Not Virtualizing Large Lists:
   // ❌ Renders all items regardless of visibility
   {bigArray.map(item => <Item {...item} />)}
  1. Inefficient Item Components:
   // ❌ Unmemoized with inline styles
   const Item = ({ data }) => (
     <div style={{ color: 'red' }}>{data.name}</div>
   );
  1. Over-fetching Data:
   // ❌ Loading all data at once
   const [items, setItems] = useState([]);
   useEffect(() => {
     fetchAllItems().then(setItems); // Thousands of items
   }, []);

Advanced Techniques

1. Custom Virtualization Hook

function useVirtualizer({ count, getItemHeight, containerRef }) {
  const [scrollTop, setScrollTop] = useState(0);
  const height = containerRef.current?.clientHeight || 0;

  const virtualItems = [];
  let startIndex = 0;
  let offsetY = 0;

  while (startIndex < count) {
    const itemHeight = getItemHeight(startIndex);
    if (offsetY + itemHeight > scrollTop) break;
    offsetY += itemHeight;
    startIndex++;
  }

  let endIndex = startIndex;
  while (endIndex < count && offsetY < scrollTop + height) {
    const itemHeight = getItemHeight(endIndex);
    virtualItems.push({ index: endIndex, offset: offsetY });
    offsetY += itemHeight;
    endIndex++;
  }

  return { virtualItems, totalHeight: offsetY };
}

2. Grid Virtualization

import { FixedSizeGrid as Grid } from 'react-window';

function Matrix({ data }) {
  return (
    <Grid
      columnCount={10}
      columnWidth={100}
      height={600}
      rowCount={1000}
      rowHeight={50}
      width={1000}
    >
      {({ columnIndex, rowIndex, style }) => (
        <div style={style}>
          {data[rowIndex][columnIndex]}
        </div>
      )}
    </Grid>
  );
}

Key Takeaways

  1. Virtualization is Essential: For lists with 100+ items
  2. Choose the Right Solution: Based on your specific needs
  3. Measure Performance: Use React DevTools Profiler
  4. Progressive Loading: Consider infinite scroll or pagination
  5. Optimize Items: Each rendered item should be lightweight

Remember: The goal is to maintain a smooth user experience while displaying large datasets. Virtualization libraries like react-window and react-virtualized provide battle-tested solutions that can dramatically improve performance.

Leave a Reply

Your email address will not be published. Required fields are marked *