Rendering large lists without virtualization causing slow UI

Loading

Optimizing Large List Rendering in React

A common performance issue in React applications occurs when rendering large lists without virtualization, causing sluggish UI and poor user experience. Here’s how to properly handle large datasets.

The Problem (Unoptimized List Rendering)

// ❌ Poor performance with large datasets
function BigList({ items }) {
  return (
    <div>
      {items.map(item => (
        <ListItem key={item.id} item={item} />
      ))}
    </div>
  );
}

// Usage with 10,000 items - will freeze the UI
<BigList items={hugeArray} />

Why this is problematic:

  1. Renders all items at once (DOM overload)
  2. Causes slow initial render
  3. Makes scrolling janky
  4. Wastes memory with off-screen elements
  5. Triggers excessive re-renders

Correct Solutions

1. Using React-Window (Recommended)

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

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

2. Using React-Virtualized

import { List } from 'react-virtualized';

function BigList({ items }) {
  return (
    <List
      width={300}
      height={600}
      rowCount={items.length}
      rowHeight={50}
      rowRenderer={({ index, key, style }) => (
        <div key={key} style={style}>
          <ListItem item={items[index]} />
        </div>
      )}
    />
  );
}

3. Pagination Approach

function PaginatedList({ items }) {
  const [page, setPage] = useState(0);
  const itemsPerPage = 50;
  const pageCount = Math.ceil(items.length / itemsPerPage);
  const visibleItems = items.slice(
    page * itemsPerPage,
    (page + 1) * itemsPerPage
  );

  return (
    <div>
      {visibleItems.map(item => (
        <ListItem key={item.id} item={item} />
      ))}
      <Pagination
        pageCount={pageCount}
        onPageChange={({ selected }) => setPage(selected)}
      />
    </div>
  );
}

Key Optimization Techniques

  1. Windowing/Virtualization:
  • Only renders visible items
  • Recycles DOM nodes
  • Maintains scroll position
  1. Pagination:
  • Splits data into chunks
  • Simple to implement
  • Good for known-length data
  1. Infinite Loading:
  • Loads more items as user scrolls
  • Works well with APIs
  • Requires scroll detection

Common Mistakes to Avoid

  1. Using array indexes as keys:
   {items.map((item, index) => (
     <ListItem key={index} /> // ❌ Bad for reordering
   ))}
  1. Complex item components:
  • Heavy renders per item compound performance issues
  1. No memoization:
   // ❌ Re-renders all items when parent updates
   const ListItem = ({ item }) => <div>{item.name}</div>;
  1. Ignoring overscan:
  • Blank areas during fast scrolling

Best Practices

  1. Always use stable keys from your data
  2. Memoize list items:
   const MemoizedListItem = React.memo(ListItem);
  1. Implement overscan for smooth scrolling:
   <List overscanCount={5} {...otherProps} />
  1. Measure performance with React DevTools
  2. Consider intersection observers for custom solutions

Advanced Patterns

1. Dynamic Item Sizes

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>
  );
}

2. Infinite Loading with Virtualization

import { useInfiniteLoader, List } from 'react-virtualized';

function InfiniteList({ items, loadMoreRows }) {
  const loadMore = useInfiniteLoader({
    isRowLoaded: ({ index }) => !!items[index],
    loadMoreRows,
    rowCount: items.length + 1, // +1 for loading indicator
  });

  return (
    <List
      width={300}
      height={600}
      rowCount={items.length}
      rowHeight={50}
      onRowsRendered={loadMore}
      rowRenderer={({ index, key, style }) => (
        <div key={key} style={style}>
          {items[index] ? (
            <ListItem item={items[index]} />
          ) : (
            <LoadingIndicator />
          )}
        </div>
      )}
    />
  );
}

3. Custom Virtualization Hook

function useVirtualizer({ size, parentRef, estimateSize }) {
  const [scrollTop, setScrollTop] = useState(0);
  const height = parentRef.current?.clientHeight || 0;

  const virtualItems = useMemo(() => {
    const rangeStart = scrollTop;
    const rangeEnd = scrollTop + height;

    let startIndex = 0;
    let endIndex = 0;
    let totalSize = 0;

    // Calculate visible range
    for (let i = 0; i < size; i++) {
      const itemSize = estimateSize(i);
      if (totalSize <= rangeEnd) endIndex = i;
      if (totalSize < rangeStart) startIndex = i;
      totalSize += itemSize;
    }

    return { startIndex, endIndex, totalSize };
  }, [scrollTop, height, size, estimateSize]);

  const onScroll = useCallback(e => {
    setScrollTop(e.currentTarget.scrollTop);
  }, []);

  return { virtualItems, onScroll };
}

Remember: For lists with more than 100-200 items, always consider virtualization. The exact threshold depends on:

  • Complexity of your list items
  • Target device capabilities
  • User experience requirements

Popular virtualization libraries include:

Leave a Reply

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