Rendering Big Ol' Lists

I was talking to someone about pagination recently and started discussing the following case: If you are paginating a request, and say rendering 20 more items on scroll, what happens if the user keeps scrolling, and scrolling, and scrolling...? Well, you'd end up with an enormous list of items.

Have you ever tried rendering 50,000 items in a browser? That is ALOT of DOM nodes and unsurprisingly the browser doesn't like it much. So how can this be handled? Here are a few techniques for handling very large data sets.

1: Use react-window Instead of rendering everything, windowing (or virtualization) only renders what’s visible in the viewport + a small buffer.

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

const Row = ({ index, style, data }) => (
  <div style={style} className="px-4 py-2 border-b">
    {index + 1}. {data[index].name}
  </div>
);

export default function LargeList({ items }) {
  return (
    <List
      height={600}          // viewport height
      itemCount={items.length}
      itemSize={48}         // row height
      width="100%"
      itemData={items}
      overscanCount={5}     // buffer above/below
    >
      {Row}
    </List>
  );
}

Now, instead of 50,000 DOM nodes, the browser only deals with ~30–40 at a time. Smooth scrolling, minimal memory usage

2: React Memo Even with windowing, React can still re-render list items unnecessarily. For example, when parent state updates, every visible row re-renders by default.

Use React.memo to prevent wasted renders.

const Row = React.memo(({ index, style, data }) => {
  const item = data[index];
  return (
    <div style={style} className="px-4 py-2 border-b">
      {index + 1}. {item.name}
    </div>
  );
});

Passing unstable references (like inline functions or new objects) can defeat memoisation.

Bad:

<List itemData={{ items }} ...>   // new object on every render

Better:

const itemData = useMemo(() => items, [items]);

<List itemData={itemData} ... />

Or for event handlers:

const handleClick = useCallback((id) => {
  console.log("Clicked", id);
}, []);

<Row onClick={handleClick} ... />

This keeps references stable and allows memo to do its job.