How to Handle Performance Issues in React When Your Data Grows Fast
At some point, every developer working with React runs into a familiar problem: a slow table. Everything works smoothly when you’re displaying a dozen rows. But once the data grows into the hundreds or thousands, the app starts to feel sluggish. The UI lags, the spinner spins forever, and clicking around feels like swimming through molasses. What’s going on?
📌 So, What’s the Real Issue?
The tricky part isn’t just the slow table — it’s figuring out why it’s slow.
Sometimes, it’s unnecessary re-renders. Other times, it’s heavy calculations or the fact that you’re rendering all 5,000 rows at once, even though the user only sees 20. That’s just wasted effort.
Real-World Strategies That Actually Work
1. Eliminate Unnecessary Re-renders
React wants to keep everything up to date, so it re-renders components if it thinks something changed. But often, nothing meaningful has changed — and still, the component re-renders.
What helps:
React.memo()
– wrap a component to prevent re-rendering if props haven’t changed.useCallback()
– memoizes a function so its reference stays the same between renders.useMemo()
– memoizes the result of a heavy calculation.
Tip: Don’t wrap everything blindly. These tools have overhead too. Use them only where it makes a measurable difference.
How to Know What’s Slowing Things Down
1. Use the React DevTools Profiler
- Install the React DevTools browser extension
- Open the Profiler tab
- Click “Record” and interact with your app
- See which components re-render often and how long they take
2. Use why-did-you-render
This library logs when and why a component re-renders, even if it probably shouldn’t:
import React from 'react';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React);
}
3. Sprinkle in Console Logs
function ProductRow({ product }) {
console.log('Render: ProductRow', product.id);
return <div>{product.name}</div>;
}
If it logs every time even when the data hasn’t changed — that’s a good candidate for React.memo()
.
4. Think Like the User
- What changes often? (e.g. filters, search bar)
- What stays stable? (e.g. table rows)
Only wrap the parts that remain stable — otherwise you’re optimizing the wrong thing.
Simple rule: If a component re-renders frequently but receives the same props, wrap it in
React.memo()
.
List Virtualization
This is a game-changer for tables and large lists.
Instead of rendering all 10,000 rows at once, why not just show the 30 that are currently visible? That’s what virtualization does.
Libraries:
react-window
– fast and simplereact-virtualized
– more powerful, more complex
Even if you have “only” 500 items — use virtualization. It’s worth it.
3. Preventing Memory Leaks
Memory leaks are sneaky. They show up when you forget to clean up things like timers, fetch calls, or event listeners. This gets worse if your table updates frequently (e.g. via polling or WebSocket).
Common leak sources:
- Intervals, timeouts, fetch requests
- WebSocket connections
- Event listeners
How to prevent them:
useEffect(() => {
const id = setInterval(() => { ... }, 1000);
return () => clearInterval(id);
}, []);
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal });
return () => controller.abort();
}, []);
Bonus Topics That Matter Too
Use Stable and Meaningful Keys
When rendering lists, always provide a unique key
:
// Bad
key={index}
// Good
key={user.id}
Profiling Tools
- React DevTools Profiler – visual breakdown of what renders and why
- why-did-you-render – console logs when something re-renders unexpectedly
Pagination and Lazy Loading
Don’t load and render everything at once. Instead:
- Use pagination (e.g. 50 rows per page)
- Or infinite scroll (load more on scroll)
Optimize Around the Table
Sometimes it’s not the table that’s slow — it’s everything around it.
- Filters that recalculate on every keystroke
- Buttons that re-trigger renders
Solution: Move them into memoized components if needed.
Preprocess Data Before Rendering
If you’re fetching 10,000 records, don’t dump them all into your table immediately.
- Filter and process on the server if possible
- Preprocess once and cache the results
- Trim down deep objects to only the needed fields
What You Should Remember
- Unnecessary re-renders are your #1 performance enemy
React.memo
,useMemo
,useCallback
help — but don’t overuse themreact-window
is your best friend for long lists- Use DevTools — they’ll show you what’s really happening
Appendix: Core Concepts Explained
What is virtualization and why is it useful?
Virtualization means rendering only the elements currently visible on the screen. This reduces the work your browser has to do and keeps the app responsive.
If you try to render 10,000 items at once, your browser might freeze. But you only need to show 20–30 rows — the rest can wait.
useMemo vs useCallback
- useMemo: memoizes a computed value
- useCallback: memoizes a function
const heavyResult = useMemo(() => calculate(data), [data]);
const handler = useCallback(() => doSomething(), []);
What does React.memo do?
It wraps a component and tells React: “If props didn’t change, skip the re-render.”
const Row = React.memo(function Row({ item }) {
return <div>{item.name}</div>;
});
Note: it compares props by reference. If the object is recreated every time — it will still trigger a re-render.
Can I combine virtualization and pagination?
Yes — and it’s often the best approach. Pagination reduces the amount of data fetched from the server. Virtualization reduces what’s rendered on screen. Win-win.
How do I prevent memory leaks?
- Clean up intervals, subscriptions, and fetches
- Use
AbortController
for fetch - Return cleanup functions in
useEffect