How would you optimize a React application rendering one hundred thousand items in a list?
Don't render 100k items. Virtualize so only the visible ~30 are mounted (react-window, TanStack Virtual). Stable ids as keys. Memoize the row component. Move filter/sort into useMemo or a Web Worker. Paginate or chunk loading from the API. For tables: pin a header, scroll only the body. For search: index once, filter the index, not the full list.
100k items rendered as 100k DOM nodes will crash any browser. Virtualization is non-negotiable.
1. Virtualize
import { FixedSizeList } from 'react-window';
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={48}
width="100%"
>
{({ index, style }) => (
<div style={style}>
<Row item={items[index]} />
</div>
)}
</FixedSizeList>Only ~30 rows in the DOM at once. Initial paint drops from seconds to milliseconds.
For dynamic heights: VariableSizeList or TanStack Virtual.
2. Memoize the row
const Row = memo(function Row({ item }: { item: Item }) {
return <div>{item.name}</div>;
});When the parent re-renders (filter change, etc.), unchanged rows skip work.
3. Stable keys
If you're not using a virtual list, keys must be ids — never index.
4. Move heavy work off the render path
Sorting/filtering 100k items every render is the next bottleneck.
const filtered = useMemo(
() => items.filter(predicate).sort(compare),
[items, predicate, compare],
);If the predicate/compare are still slow, move into a Web Worker:
self.onmessage = ({ data: { items, query } }) => {
self.postMessage(items.filter(i => i.name.includes(query)));
};Main thread stays at 60fps; results stream back.
5. Paginate the data fetch
100k items at once is rarely necessary. Server pagination + infinite scroll loads as needed.
const { data, fetchNextPage } = useInfiniteQuery({
queryKey: ['items'],
queryFn: ({ pageParam = 0 }) => fetch(`/items?cursor=${pageParam}`),
getNextPageParam: last => last.nextCursor,
});
const items = data?.pages.flatMap(p => p.items) ?? [];6. Index for search
Full-text scan per keystroke is slow. Pre-index:
import Fuse from 'fuse.js';
const fuse = new Fuse(items, { keys: ['name', 'tags'] });
const results = fuse.search(query);Build the index once (useMemo on items), reuse on every search.
7. Defer non-urgent updates
const [query, setQuery] = useState('');
const deferred = useDeferredValue(query);
const results = useMemo(() => search(deferred), [deferred]);Input stays responsive; result list lags during heavy work but doesn't block typing.
8. Tables specifically
- Pin header outside the scroll container.
- Sticky column-grouping uses CSS position sticky — works with virtualization.
- For sortable/filterable tables: TanStack Table + TanStack Virtual is the standard.
9. Image-heavy rows
Lazy-load images per row (loading=lazy) so off-screen rows don't fetch.
10. Memory
Even with virtualization, holding 100k objects has a cost. If items are heavy, normalize and store only what the row needs.
Profiling
- React DevTools Profiler: confirm only visible rows render on scroll.
- DevTools Performance: scroll at 60fps; look for long tasks > 50ms.
- Memory tab: check heap snapshot after loading 100k items.
Anti-patterns
- items.slice(0, 100).map(...) and a 'load more' button — unworkable as the threshold for 'enough'.
- CSS overflow scroll on a 100k-row list without virtualization — initial paint blocks for seconds.
- Re-creating the index on every keystroke instead of once.
Follow-up questions
- •How do you handle variable row heights in a virtualized list?
- •When would a Web Worker pay off for filtering?
- •What's the right key to use in a 100k-item virtualized list?
Common mistakes
- •Mounting 100k DOM nodes with overflow auto and hoping the browser figures it out.
- •Re-building the search index on every render — defeats indexing.
- •Virtualizing without memoizing the row — scroll still triggers expensive re-renders.
Performance considerations
- •DOM is the bottleneck. 100k nodes lay out and paint in seconds. ~30 visible nodes lay out in milliseconds. After virtualization, the next bottleneck is usually JavaScript (filter/sort/search), addressed with indexing + workers.
Edge cases
- •Find-in-page (Ctrl+F) only searches rendered text — virtualization hides 99% of items.
- •Selecting across off-screen rows requires selection state outside the virtual scroller.
- •Focus jumps when rows unmount on scroll — manage focus explicitly for keyboard users.
Real-world examples
- •GitHub's issue list, Slack's message search, VSCode's file picker, Linear's issue browser, Notion's database views. All virtualize, index search, paginate fetch.