DEV Community

Cover image for How to Render 5,000 Items in React Without Crashing the Browser
jeetvora331
jeetvora331

Posted on

How to Render 5,000 Items in React Without Crashing the Browser

If you have ever tried to render a big list in React, you already know the pain. The page freezes. The scroll lags. The browser gives up.

In this article we will understand why this happens and how to fix it properly, with code.

Why does rendering 5,000 items cause problems?

When React renders a list, it eventually has to talk to the real browser DOM. And the browser DOM is slow when you throw thousands of nodes at it.

There are two reasons for the lag:

1. Initial Mount
The browser has to parse and insert all 5,000 nodes when the page loads. Your page feels slow before the user even does anything.

2. Layout and Reflow
Every time an item updates, the browser recalculates the position of elements on the page. With 5,000 nodes, this is very slow. Like asking someone to reorganize a warehouse every time one box moves.

The problem is not React. The problem is asking the browser to manage too many DOM nodes at once.

The real solution: List Virtualization (Windowing)

The idea is simple:

Instead of rendering all 5,000 items, only render the ones currently visible on the screen.

If the user can only see 15 items at a time, you only put 15 items in the DOM. As the user scrolls, you swap them out.

Here is how it works:

  • The container div has a fixed height and overflow-y: auto so it scrolls.
  • A tall inner div simulates the full height of all 5,000 items so the scrollbar looks correct.
  • JavaScript figures out which items are visible and renders only those using position: absolute.

Explaining diagram


Option 1: Use a library (for real projects)

For production apps, use react-window. It is lightweight and handles everything.

npm install react-window
Enter fullscreen mode Exit fullscreen mode
import { FixedSizeList as List } from 'react-window';

const Row = ({ index, style, data }) => (
  <div style={style}>
    {data[index].name}
  </div>
);

const MyList = ({ items }) => (
  <List
    height={500}       // how tall the visible area is
    itemCount={5000}   // total items
    itemSize={35}      // height of each row
    width={300}
    itemData={items}
  >
    {Row}
  </List>
);
Enter fullscreen mode Exit fullscreen mode

That is all you need. react-window handles scroll events, index calculations, buffer items, everything.

Option 2: Build it yourself (for interviews)

If an interviewer asks you to build basic virtualization from scratch, here is the logic:

  1. Listen to the scroll event.
  2. Based on scrollTop, calculate which items should be visible.
  3. Render only those items using absolute positioning.
import React, { useState } from 'react';

const ITEM_HEIGHT = 40;
const VIEWPORT_HEIGHT = 400;
const BUFFER = 2;

export function CustomVirtualList({ items }) {
  const [scrollTop, setScrollTop] = useState(0);

  const totalHeight = items.length * ITEM_HEIGHT;

  const startIndex = Math.max(
    0,
    Math.floor(scrollTop / ITEM_HEIGHT) - BUFFER
  );

  const endIndex = Math.min(
    items.length - 1,
    Math.floor((scrollTop + VIEWPORT_HEIGHT) / ITEM_HEIGHT) + BUFFER
  );

  const visibleItems = items.slice(startIndex, endIndex + 1);

  const handleScroll = (e) => {
    setScrollTop(e.currentTarget.scrollTop);
  };

  return (
    <div
      onScroll={handleScroll}
      style={{ height: VIEWPORT_HEIGHT, overflowY: 'auto', position: 'relative' }}
    >
      <div style={{ height: totalHeight, width: '100%' }}>
        {visibleItems.map((item, index) => {
          const actualIndex = startIndex + index;
          return (
            <div
              key={item.id}
              style={{
                position: 'absolute',
                top: actualIndex * ITEM_HEIGHT,
                height: ITEM_HEIGHT,
                width: '100%',
              }}
            >
              {item.text}
            </div>
          );
        })}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Walk through this slowly:

  • startIndex is the first item to render, a bit above the visible area.
  • endIndex is the last item, a bit below.
  • visibleItems is just the slice of data we need right now.
  • Each item is placed at the right pixel position using top: actualIndex * ITEM_HEIGHT. Pretty simple once you see it.

Why do we need position: absolute?

This part confuses a lot of people. Let me explain.

Normally in HTML, elements flow one after another from the top. Item 1 at the top, item 2 below it, and so on. This is normal document flow.

The problem is: if you only have 15 items in the DOM but they always start from the top, item 43 will visually appear at y = 0 instead of where it should be. The list looks completely broken.

You need a way to say: "even though item 43 is one of only 15 nodes in the DOM, place it at the exact pixel position it would be at if all 5,000 items were there."

That is what position: absolute with a calculated top value does.

style={{
  position: 'absolute',
  top: actualIndex * ITEM_HEIGHT,  // item 43 -> 43 * 40 = 1720px from top
  height: ITEM_HEIGHT,
  width: '100%',
}}
Enter fullscreen mode Exit fullscreen mode

So item 43 gets top: 1720px. Item 44 gets top: 1760px. They land exactly where they should visually be.

The outer <div> has position: relative so these absolute values are calculated relative to the container, not the whole page. The big inner div fakes the full height for a realistic scrollbar. Together they create the illusion of 5,000 items when only 15 actually exist in the DOM.

What if virtualization is not an option?

Sometimes you cannot use windowing. For example, if the user needs to use Ctrl+F to search the page, all content must be in the DOM.

In that case you have two options:

Pagination - Break the data into pages of 50 or 100. The user clicks "Next" to load more. Simple, works great for most cases.

Infinite Scroll - Load the first 50 items. When the user reaches the bottom, load the next 50 and append them using IntersectionObserver.

const observer = new IntersectionObserver((entries) => {
  if (entries[0].isIntersecting) {
    loadMoreItems();
  }
});

observer.observe(bottomRef.current);
Enter fullscreen mode Exit fullscreen mode

Small things that make a big difference

Even with virtualization, individual items can still cause slowness.

Use unique keys, not array index

// Bad
items.map((item, index) => <Row key={index} />)

// Good
items.map((item) => <Row key={item.id} />)
Enter fullscreen mode Exit fullscreen mode

Using index as the key confuses React when items are reordered or deleted. Always use a unique ID.

Wrap items with React.memo

const ListItem = React.memo(({ item }) => {
  return <div>{item.name}</div>;
});
Enter fullscreen mode Exit fullscreen mode

Without this, every item re-renders whenever the parent state updates.

Quick summary

Problem Solution
Too many DOM nodes List virtualization (react-window)
Need Ctrl+F search Pagination or Infinite Scroll
Items re-rendering too much React.memo + unique keys
Breaking memoization useCallback for handlers

Rendering large lists looks scary at first but once you understand the DOM bottleneck it is straightforward. And in an interview, explaining why virtualization works is what separates a good answer from a great one.

If you found this helpful, drop a reaction and let me know in the comments what topic you want next!

Top comments (0)