Back to React
React
medium
mid

How would you optimize React rendering performance for large lists, for example ten thousand rows?

Virtualize (TanStack Virtual / react-window) — render only visible rows. Memoize the row component. Stable keys. Avoid layout reads in row render. Defer non-urgent updates with `useDeferredValue` or `startTransition`. For variable heights use measurement cache. Server-side filter/sort for huge datasets so the client only handles a window. Don't memoize the list itself — focus on rows.

4 min read·~15 min to think through

Levers

1. Virtualization (the big win)

tsx
import { useVirtualizer } from '@tanstack/react-virtual';

function List({ items }) {
  const parentRef = useRef(null);
  const v = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 40,
    overscan: 8,
  });
  return (
    <div ref={parentRef} style={{ height: 600, overflow: 'auto' }}>
      <div style={{ height: v.getTotalSize(), position: 'relative' }}>
        {v.getVirtualItems().map((vi) => (
          <Row key={items[vi.index].id} item={items[vi.index]} style={{ position: 'absolute', top: vi.start, height: vi.size, width: '100%' }} />
        ))}
      </div>
    </div>
  );
}

DOM nodes ~constant; scrolling stays smooth.

2. Memoize the Row

tsx
const Row = React.memo(({ item }) => <div>{item.label}</div>);

Memo so unrelated state changes don't re-render every visible row.

3. Stable keys

Use item.id, not array index. Indices break memo identity across reorders / filters.

4. Stable prop refs

Callbacks passed to Row should be stable:

tsx
const onSelect = useCallback((id) => dispatch(select(id)), []);

Otherwise memo bails.

5. Avoid layout reads in row render

Reading offsetWidth in a row → forced sync layout on every render.

6. Defer non-urgent updates

For filters / search:

tsx
const deferred = useDeferredValue(query);
const filtered = useMemo(() => items.filter(...), [items, deferred]);

Or startTransition around the filter set.

7. Server-side filter/sort/page

If items is 100k+, don't ship them all. Cursor + server filter. Client only handles a window.

8. Variable row heights

TanStack Virtual measures rows as they render; updates total height. For known heights, set estimateSize accurately.

9. Worker for derivation

Big sort/filter on 100k items → Web Worker. Comlink for ergonomic message passing.

10. Suspense + transitions for fetch

Background-fetch new pages with useTransition so the visible list stays interactive.

What NOT to do

  • Memoize the list component (it always re-renders when items change).
  • Sprinkle useCallback on handlers that aren't passed to memoized children.
  • Virtualize 50-row lists (overhead > gain).

Measurement

React Profiler: time on render. Watch for rows re-rendering when they shouldn't. Chrome Performance: scroll FPS; long tasks during scroll. Memory: heap should be ~constant under scroll.

Interview framing

"Virtualize — the single biggest win on 10k+ rows; render only the visible window + overscan. Memoize the Row component with stable keys (item.id) and stable callback refs from the parent. Avoid layout reads in row render to prevent forced sync layout. Defer non-urgent updates (filter/sort) with useDeferredValue or startTransition so input stays snappy. For massive data, server-side filter/sort/paginate — don't ship 100k rows to the client. Web Worker for expensive derivations. Adopt TanStack Virtual instead of rolling your own — variable heights, sticky headers, anchored prepends are full of edge cases."

Follow-up questions

  • When is virtualization the wrong choice?
  • How do you handle variable row heights?
  • Why memoize Row but not the List?

Common mistakes

  • Virtualizing small lists.
  • Index keys.
  • Unstable callback refs.
  • Layout reads in row render.

Performance considerations

  • O(visible) DOM nodes; memo keeps re-renders scoped.

Edge cases

  • Anchored prepend (chat scrolling up).
  • Sticky headers.
  • Variable heights with measurement.
  • Ctrl-F doesn't find unrendered rows.

Real-world examples

  • Slack message lists, Linear issue lists, Notion databases.

Senior engineer discussion

Seniors adopt libraries (TanStack Virtual) and design server-side pagination for genuinely large datasets.

Related questions