How to optimize large lists in React
Rendering thousands of DOM nodes in a React list causes severe performance degradation — slow initial render, janky scrolling, and high memory usage.
As the creator of CoreUI with 25 years of front-end development experience, I’ve optimized data-heavy tables and lists in CoreUI dashboards that display thousands of rows, and virtualization is the single most impactful technique.
The approach with react-window renders only the rows visible in the viewport, keeping the DOM node count constant regardless of data size.
A list with 10,000 items renders the same ~20 DOM nodes as a list with 10 items when virtualized.
Install react-window and create a virtualized list.
npm install react-window
// VirtualList.jsx
import { FixedSizeList } from 'react-window'
const ITEM_HEIGHT = 50
function Row({ index, style, data }) {
const item = data[index]
return (
// style must be applied to the row for positioning
<div style={style} className="list-row">
<span>{item.id}</span>
<span>{item.name}</span>
<span>{item.email}</span>
</div>
)
}
export function VirtualList({ items }) {
return (
<FixedSizeList
height={500} // Visible height of the list container
itemCount={items.length}
itemSize={ITEM_HEIGHT}
width="100%"
itemData={items} // Pass data via itemData for stable reference
>
{Row}
</FixedSizeList>
)
}
FixedSizeList assumes all rows have the same height, making it the most performant option. The style prop from react-window positions each row absolutely — always apply it to the row element. itemData passes your data array without creating a new closure on each render.
Variable Height Rows
Use VariableSizeList when rows have different heights.
import { VariableSizeList } from 'react-window'
const ITEM_SIZES = [40, 80, 55, 120, 40] // Different heights
function VariableRow({ index, style, data }) {
return (
<div style={style} className="list-row">
{data[index].content}
</div>
)
}
export function VariableList({ items }) {
const getItemSize = (index) => ITEM_SIZES[index % ITEM_SIZES.length]
return (
<VariableSizeList
height={600}
itemCount={items.length}
itemSize={getItemSize}
width="100%"
itemData={items}
>
{VariableRow}
</VariableSizeList>
)
}
itemSize is a function that returns the height for each index. If you don’t know item heights in advance, measure them with ResizeObserver and pass the measured heights.
Virtualized Grid (Tables)
Use FixedSizeGrid for tabular data with many columns.
import { FixedSizeGrid } from 'react-window'
function Cell({ columnIndex, rowIndex, style, data }) {
return (
<div style={style} className="grid-cell">
{data[rowIndex][columnIndex]}
</div>
)
}
export function VirtualTable({ rows, columns }) {
return (
<FixedSizeGrid
columnCount={columns.length}
columnWidth={150}
height={500}
rowCount={rows.length}
rowHeight={40}
width={800}
itemData={rows}
>
{Cell}
</FixedSizeGrid>
)
}
FixedSizeGrid virtualizes both rows and columns, making it suitable for wide spreadsheet-like tables with thousands of rows and many columns.
Best Practice Note
This is the same virtualization approach used in CoreUI React data table templates for handling large datasets. Pair virtualization with React.memo on row components to prevent re-renders when unrelated state changes. For tables that need sorting, filtering, and pagination, consider TanStack Table (react-table) which integrates well with react-window and handles the data transformation layer. See how to prevent unnecessary re-renders in React for complementary rendering optimizations.



