Hey everyone, I’m Prashuk. If you’ve been building web apps for a while, you know that nothing kills a smooth UI faster than a massive data table.
We’ve all been there—you fetch a couple of thousand rows from the API, map through them, and suddenly the browser feels like it’s running on a toaster. Scrolling becomes jerky, and clicking a row takes a full second to register.
In one of my recent projects, I had to build a dashboard that displayed live transaction logs. We were talking about 10,000+ entries. I initially thought, "I'll just paginate it," but the client insisted on a "smooth, infinite scrolling experience."
That’s when I had to dive deep into virtualization. Here’s how I handled it without sacrificing accessibility or clean code.
1. Why standard mapping fails
The problem isn't React; it's the DOM. If you render 5,000 rows, each with 5-10 columns, you’re asking the browser to manage 50,000 nodes. Every time a state changes, the reconciler has to work overtime.
I ran into this issue when I noticed that even a simple hover effect on a row was lagging. I realized that rendering everything at once is just bad practice for performance. What worked for me was shifting to a "render-only-what-you-see" approach.
2. Choosing the right tool (Keep it simple)
You could write your own virtualization logic using onScroll and scrollTop math, but honestly? Don't reinvent the wheel unless you have to.
I prefer using react-window or react-virtuoso. For most of my MERN projects, react-window is the sweet spot because it’s tiny and does exactly what it says on the tin. It keeps the DOM light by only rendering the rows currently visible in the viewport plus a small "buffer" above and below.
3. The Accessibility "Gotcha"
This is where most people (including me, initially) mess up. When you virtualize a table, you’re constantly adding and removing elements from the DOM. Screen readers get very confused because the "total" list seems to change as you scroll.
To fix this, you can't just use a standard <table> tag easily because virtualization usually requires position: absolute on rows.
What worked for me:
- Use
role="grid"on the container. - Use
role="row"androle="gridcell"for the children. - Ensure the container has an
aria-rowcountso the screen reader knows there are 10,000 items, even if only 10 are visible.
4. Implementation (The Practical Way)
Here’s a simplified version of how I usually set this up. I focus on keeping the row component memoized to avoid unnecessary re-renders.
import { FixedSizeList as List } from 'react-window';
import React, { memo } from 'react';
// Memoizing the row is huge for performance
const Row = memo(({ index, style, data }) => {
const item = data[index];
return (
<div style={style} className="table-row" role="row">
<div role="gridcell">{item.id}</div>
<div role="gridcell">{item.name}</div>
<div role="gridcell">{item.status}</div>
</div>
);
});
const VirtualTable = ({ items }) => {
return (
<div role="grid" aria-rowcount={items.length} className="table-container">
<List
height={500}
itemCount={items.length}
itemSize={50} // Height of each row
width={"100%"}
itemData={items}
>
{Row}
</List>
</div>
);
};
5. Styling and UI polish
Performance is great, but if the UI jumps around, users will hate it. One thing I always do is add a subtle shadow or a border-bottom to the rows so they feel distinct.
Also, watch out for "White Flash." If a user scrolls too fast, they might see empty space before the next row renders. I usually set an overscanCount={5} in react-window. It renders 5 extra rows outside the visible area, so the user never sees the "emptiness." It’s a small detail, but it makes the app feel premium.
Best Practices I follow:
- Memoize everything: Use
React.memofor your row components. If you don't, virtualization loses half its benefit. - Fixed vs. Variable heights: Use
FixedSizeListif your rows are the same height. It’s much faster. Only useVariableSizeListif you absolutely must (like rows with varying text content). - Debounce Search: If you have a search bar above your virtualized table, debounce the input. You don't want to recalculate a 10,000-item filter on every keystroke.
Final Thoughts
Building high-performance tables isn't about being a math genius; it's about being smart with browser resources. By using virtualization, I reduced the initial load time of my project's dashboard from 4 seconds to under 200ms.
Don't just build for the happy path where there are 10 items. Build for the messy, real-world data that grows over time. Your users (and their CPUs) will thank you.
Catch you in the next one!