Skip to main content

Optimizing long lists

1) Virtualization (windowing) - a must-have

We render only the visible items + a small "overscan".

Libraries:

  • react-window (lightweight, modern)
  • react-virtualized (richer, heavier)
  • @tanstack/react-virtual (a puzzle, but flexible)

Example with react-window (fixed height):

javascript
import { FixedSizeList as List } from 'react-window'; import React from 'react'; const Row = React.memo(({ index, style, data }: any) => { const item = data.items[index]; return ( <div style={style} onClick={() => data.onSelect(item.id)}> {item.title} </div> ); }, (prev, next) => prev.data.items[prev.index] === next.data.items[next.index]); export default function Virtualized({ items, onSelect }: { items: any[], onSelect: (id:string)=>void }) { const itemData = React.useMemo(() => ({ items, onSelect }), [items, onSelect]); return ( <List height={600} itemCount={items.length} itemSize={48} width="100%" overscanCount={6} itemData={itemData} > {Row} </List> ); }

Variable height? Use VariableSizeList + measure the height (for example, via ResizeObserver) and cache getItemSize.


2) Pagination and "infinite scroll"

  • Load data in chunks (limit/offset or cursor).
  • Render a flat array; new batches are appended.
  • To detect the end, use IntersectionObserver (no permanent scroll handlers).

3) Minimize item re-renders

  • Each item is a separate memoized component: React.memo(Row, areEqual).
  • Item props are stable by reference:
    • handlers via useCallback;
    • complex objects/arrays via useMemo;
    • pass itemId instead of closing over item in an inline callback.
  • Don't use the index as key during inserts/deletes/reorders - use a stable id.

4) Separate computation from presentation

  • Filtering, sorting, aggregations - do them once and cache with useMemo.
  • When typing into a filter field, use useDeferredValue or wrap updates in startTransition so the list doesn't "lag":
javascript
const [query, setQuery] = React.useState(''); const deferredQuery = React.useDeferredValue(query); // smooths out filtering const filtered = React.useMemo( () => items.filter(x => x.title.includes(deferredQuery)), [items, deferredQuery] );

5) Images and heavy cells

  • Enable lazy loading: <img loading="lazy" ... />.
  • Placeholders/skeletons instead of heavy content until the data appears.
  • Avoid expensive DOM and complex CSS inside a cell (shadows, filters).

6) Scroll events

  • Don't attach a "manual" onScroll to the window. If you need to, use passive listeners and requestAnimationFrame + throttle.
  • With virtualization, the library already optimizes scrolling - less DIY needed.

7) The list container

  • Set an explicit height/max-height and overflow: auto; - it's easier for the browser.
  • For nested lists, use the CSS contain: content; (limits the rendering area).

8) Server rendering and hydration

  • For SEO/SSR of large lists, serve a limited number of items, load the rest on the client.
  • In Next.js you can stream and load chunks progressively, and virtualize on the client.

9) Size cache and DOM reuse

  • With variable height, keep a measurement cache (id → height).
  • Avoid fully resetting the cache on partial data changes.

10) Diagnostics

  • React DevTools Profiler: look for "hot" components.
  • Add markers to cell render logs (see console.log('Row', index)) locally.
  • Check whether you're creating new objects/functions in JSX.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.