🚀 Boost React Performance with Windowing: A Guide to Virtual Lists
When building rich, interactive UIs with React, you might eventually hit a serious performance bottleneck—especially if you’re rendering large lists, tables, or grids.
Thousands of DOM nodes = slow performance. And even though React is fast, it’s not magic. Thankfully, there’s a powerful solution: windowing.
In this post, you’ll learn:
@tanstack/react-virtual
Let’s explore how rendering less actually gives your users more.
React optimizes rendering in three phases:
React’s speed shines when the DOM updates are small. But what happens when your app tries to render 10,000+ elements?
Even if each element is simple, React still needs to:
That’s where things slow to a crawl.
Imagine a list with 5,000 rows and 100 columns. That’s 500,000 cells.
You might slice the data like this:
const visibleRows = data.slice(0, 500)
…but your users want to scroll forever. Removing the slice tanks performance.
They don’t care about reconciliation or commit phases. They just want the app to work. So how do you deliver a seamless experience?
Windowing (also called virtualization) is a technique where you render only the items visible in the viewport—and a few extras for smooth scrolling.
Everything else? You skip it entirely until the user scrolls.
@tanstack/react-virtual
Instead of reinventing the wheel, we’ll use @tanstack/react-virtual
, a battle-tested, flexible library for list virtualization in React.
Let’s refactor a basic list to use it.
function MyList({ items }) {return (<ul>{items.map(item => (<li key={item.id}>{item.name}</li>))}</ul>)}
This is simple but unscalable for large lists.
useVirtualizer
import { useRef } from 'react'import { useVirtualizer } from '@tanstack/react-virtual'function MyList({ items }) {const parentRef = useRef<HTMLUListElement>(null)const rowVirtualizer = useVirtualizer({count: items.length,getScrollElement: () => parentRef.current,estimateSize: () => 30, // height in px per row})return (<ulref={parentRef}style={{height: 300,overflow: 'auto',position: 'relative',}}><li style={{ height: rowVirtualizer.getTotalSize(), position: 'relative' }} />{rowVirtualizer.getVirtualItems().map(({ index, key, start, size }) => {const item = items[index]return (<likey={key}style={{position: 'absolute',top: 0,left: 0,width: '100%',height: `${size}px`,transform: `translateY(${start}px)`,}}>{item.name}</li>)})}</ul>)}
Now, only visible rows are rendered to the DOM. The rest are skipped until needed.
With the list virtualization in place, try the following:
🎉 You’ll notice:
This technique becomes even more powerful when integrated with libraries like downshift
for combo boxes and typeahead components.
If your filtered result list becomes massive, virtualization can ensure it stays snappy.
You might be tempted to just show the top 500 items and call it a day. But users like infinite scroll—they expect to see everything.
Windowing gives you both: performance + completeness.
It’s a win-win. 🎯
@tanstack/react-virtual
is more modern, flexible, and maintained by the creators of TanStack Query.
Rendering thousands of elements in React is a recipe for sluggish UI and frustrated users. With windowing, you:
✅ Render only what matters ✅ Improve perceived and actual performance ✅ Scale UI components confidently
Use @tanstack/react-virtual
, plug it into your scrollable lists, and give your users that buttery-smooth experience—even with massive datasets.
Happy optimizing! 🧑💻
Quick Links
Legal Stuff
Social Media