Designing Calm Performance for Large React Lists

Problem statement

To build a high-performance UI for a large list of cards. We had to handle a dynamic dataset that could grow up to 1000 items.

Each card included:

  • A high-resolution image
  • A title
  • A few additional metadata fields

As the number of cards increased, the page began to lag significantly. This was amplified by other concurrent features such as:

  • Countdown timers
  • Real-time updates
  • Frequent UI state changes

The challenge was to keep scrolling smooth and interactions responsive while managing heavy rendering workloads.

What was tried

1. Windowed pagination (loading 50 items at a time)

The first approach was to load only a fixed number of cards (e.g., 50) at a time.
As the user scrolled down:

  • New data was fetched and appended at the bottom
  • Older items were removed from the top to keep the total count constant
const PAGE_SIZE = 50;

const loadNextBatch = async () => {
  const nextItems = await fetchPage(currentPage + 1);
  setItems(prev =>
    [...prev.slice(PAGE_SIZE), ...nextItems]
  );
};

Issues with this approach:

  • State logic became complex due to constant add/remove operations
  • Scroll position needed manual correction to avoid jumps
  • Users noticed content shifting when items were removed from the top
  • Edge cases (fast scrolling, slow networks) caused instability

2. Infinite scroll

Infinite scrolling was introduced to make content loading feel natural.

const onScroll = () => {
  if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 200) {
    loadNextPage();
  }
};

Issues with this approach:

  • Over time, all cards were mounted in the DOM
  • Memory usage kept increasing due to high-resolution images
  • Performance degraded again near the end of the list

What worked

Viewport-based rendering (manual virtualization)

Instead of rendering everything, a selective rendering strategy was implemented:

  • Only the 6 cards fully visible in the viewport were rendered
  • Additionally, 3 cards above and 3 cards below the viewport were rendered as a buffer
  • All other cards were replaced with lightweight shimmer placeholders

To detect which cards entered or exited the viewport, IntersectionObserver was used instead of scroll math.

const observer = new IntersectionObserver(callback, {
  root: null,
  rootMargin: "300px", // buffer for top & bottom cards
  threshold: 0.1,
});

const callback: IntersectionObserverCallback = (entries) => {
  entries.forEach(entry => {
    const index = Number(entry.target.getAttribute("data-index"));

    setVisibleMap(prev => ({
      ...prev,
      [index]: entry.isIntersecting,
    }));
  });
};

Attaching the observer:

<div
  ref={el => el && observer.observe(el)}
  data-index={index}
>
  {isVisible ? <Card data={item} /> : <CardShimmer />}
</div>

To avoid rapid state updates during scroll, visibility updates were debounced:

const updateVisibility = debounce(setVisibleMap, 100);

Benefits:

  • Drastically reduced DOM complexity
  • Prevented sudden blank areas during fast scrolls
  • Maintained perceived continuity using shimmer placeholders

Using virtualization libraries (recommended solution)

This manual solution was later replaced with battle-tested libraries:

  • react-window
    • Lightweight and performance-focused
    • Ideal for large lists and grids
  • react-virtualized
    • Feature-rich with advanced layout support
    • Slightly heavier but very flexible

Example using react-window

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

<List
  height={600}
  itemCount={items.length}
  itemSize={220}
  width="100%"
>
  {({ index, style }) => (
    <div style={style}>
      <Card data={items[index]} />
    </div>
  )}
</List>

Final outcome / learning

  • Rendering many items is not the issue
  • Rendering many DOM nodes at once is
  • Viewport-based rendering is essential for large lists
  • Manual virtualization works but adds complexity
  • Libraries like react-window and react-virtualized provide cleaner, scalable solutions

By switching to virtualization, the UI stayed smooth and responsive, even with real-time updates and timers running continuously.

Resources

  1. react-window
    Lightweight and highly performant library for rendering large lists and grids.
    Best choice when item sizes are predictable and simplicity matters.
  2. react-virtualized
    A more feature-rich virtualization library with support for complex layouts, dynamic sizing, and advanced use cases.

If you’ve solved similar performance challenges in a different way, I’d love to hear your approach. There’s always room to improve how we build smooth, scalable UIs.