The Hidden Performance Killer in React Apps
Standard React lists usually start with a simple .map() call. This approach works flawlessly when you’re dealing with 50 or 100 items. However, the moment your app needs to display 10,000 rows of logs or a massive product catalog, the browser begins to stutter. You will notice a 200ms delay when typing in search bars, scrolling feels heavy, and the tab’s memory usage can easily climb past 1GB.
React isn’t the bottleneck here; the DOM is. Each node you inject requires memory and forces the browser to calculate layouts. If every list item contains five child elements, a 10,000-row list dumps 50,000 nodes into the document. Even if they are off-screen, the browser still has to keep them in memory. Virtual Scrolling, also known as Windowing, solves this bottleneck by changing how we think about the DOM.
How Virtual Scrolling Actually Works
In production-grade applications, virtualization is a non-negotiable optimization for data-heavy views. The concept is straightforward. Instead of rendering the entire list, we only render the items currently visible in the user’s viewport. We also include a small buffer of items above and below the fold to keep the scrolling experience seamless.
Think of it like a physical window in a house. You might be standing in front of a wall that is 50 meters long, but you can only see the portion of the garden visible through the 1-meter wide glass. As you walk down the hallway, the view changes, but the size of the window remains constant. In technical terms, we set a container with a massive scrollable height to keep the scrollbar accurate, but we only mount about 10 to 20 actual <div> elements at any given time.
The Core Math Behind the Technique
To build a virtualizer from scratch, you need to track three specific variables:
- Scroll Top: The current vertical scroll position in pixels.
- Viewport Height: The height of the visible area (e.g., 500px).
- Item Height: The height of a single row.
With these numbers, you can determine exactly which indices to show. If the user has scrolled down 1,000px and each row is 50px tall, the first visible item is index 20. It is simple math, but it prevents the browser from drowning in unnecessary nodes.
Hands-on: Implementing Virtual Scrolling with react-window
While you could write custom onScroll listeners, the community has already built highly optimized tools for this. I recommend react-window because it is tiny—only about 6kb gzipped. It handles complex edge cases like keyboard navigation and scroll-to-index logic out of the box. Let’s look at a basic implementation for 10,000 items.
import React from 'react';
import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style }) => (
<div style={{ ...style, borderBottom: '1px solid #eee', display: 'flex', alignItems: 'center' }}>
Row {index} - Data Point: {Math.random().toFixed(4)}
</div>
);
const VirtualList = () => {
const itemCount = 10000;
return (
<div style={{ height: '400px', width: '100%', border: '1px solid #ccc' }}>
<List
height={400}
itemCount={itemCount}
itemSize={50}
width="100%"
>
{Row}
</List>
</div>
);
};
export default VirtualList;
Open your browser’s DevTools and inspect the list while scrolling. You will see that as items move out of view, their DOM nodes are recycled or removed instantly. The total node count remains stable regardless of whether your list has 100 or 100,000 items.
Handling Dynamic Heights: The Real Challenge
Fixed heights are easy to calculate, but real-world data is rarely uniform. What if one row has a single sentence and the next has three paragraphs? If you don’t know the height of item #500, you cannot accurately calculate the total scrollable area or the position of the scrollbar thumb.
Modern apps usually tackle this using Estimated Heights. Libraries like @tanstack/react-virtual allow you to provide a best-guess height. Once a row actually renders, the library measures the real DOM node and updates the entire list layout dynamically. This prevents “jumping” as the user scrolls through content with varying lengths.
Here is how @tanstack/react-virtual handles these dynamic scenarios:
import { useVirtualizer } from '@tanstack/react-virtual';
function DynamicList({ items }) {
const parentRef = React.useRef();
const rowVirtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 100, // Our best guess
});
return (
<div ref={parentRef} style={{ height: '500px', overflow: 'auto' }}>
<div style={{ height: `${rowVirtualizer.getTotalSize()}px`, width: '100%', position: 'relative' }}>
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.key}
data-index={virtualRow.index}
ref={rowVirtualizer.measureElement}
style={{ position: 'absolute', top: 0, left: 0, width: '100%', transform: `translateY(${virtualRow.start}px)` }}
>
{items[virtualRow.index].content}
</div>
))}
</div>
</div>
);
}
Best Practices for a Smooth Experience
A virtualized list can still feel “janky” if not configured correctly. To ensure 60fps performance, follow these three rules from my past project audits.
1. Use Overscan for Buffer
Overscan renders a few extra items just outside the visible viewport. If a user scrolls quickly, this buffer ensures they see content immediately rather than a blank white flash. Setting an overscan of 5 to 10 items is usually enough to mask the rendering lag on most mobile devices.
2. Memoize Row Components
The virtualizer triggers re-renders constantly during a scroll event. If your row component is complex, use React.memo to prevent React from re-calculating the internal logic of every row that stays visible. This small change can reduce CPU usage by 30% during fast scrolls.
3. Keep Row Logic Lean
Avoid heavy data processing or date formatting inside the Row component. If you need to format 10,000 dates, do it once when the data is fetched. Passing pre-formatted strings to your list is significantly faster than calculating them on every frame.
Conclusion
Virtual scrolling is a fundamental optimization for modern web apps. By moving away from a “render everything” mindset, you can build interfaces that stay responsive even with massive datasets.
Whether you choose react-window for its lightweight footprint or TanStack Virtual for complex dynamic layouts, the impact on user experience is immediate. Audit your current project for any list longer than 200 items; replacing them with a virtualized version is often the biggest performance win you can achieve.

