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
itemIdinstead of closing overitemin an inline callback.
- handlers via
- Don't use the index as
keyduring inserts/deletes/reorders - use a stableid.
4) Separate computation from presentation
- Filtering, sorting, aggregations - do them once and cache with
useMemo. - When typing into a filter field, use
useDeferredValueor wrap updates instartTransitionso 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"
onScrollto the window. If you need to, use passive listeners andrequestAnimationFrame+throttle. - With virtualization, the library already optimizes scrolling - less DIY needed.
7) The list container
- Set an explicit height/
max-heightandoverflow: 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 readyPremium
A concise answer to help you respond confidently on this topic during an interview.