My List Is Slow But I Have No Idea Why
I built a dashboard that displays about 100 items in a list. It ran fine locally, but as the data grew, it got noticeably slower. Every scroll felt janky, and changing the search filter froze the UI for 1-2 seconds.
The problem wasn't that it was slow—the problem was I had no idea where the slowness came from. The code looked fine. I sprinkled console.log everywhere, but I couldn't tell when or why components were re-rendering. I'd log a component render and see it fire constantly, but I had no idea if that was normal or a bug.
Then a coworker asked: "Did you try the DevTools Profiler?"
Oh right. That exists. I'd been using Chrome DevTools exclusively and had installed React DevTools but never really used it. I opened it and saw two tabs: Components and Profiler. I had no idea what the difference was, so I just hit the Profiler first.
Seeing the Culprit With My Own Eyes
I clicked the blue record button in the Profiler tab. Then I changed the search filter in my app. Instantly, a colorful flame graph appeared in DevTools. Each component showed how much rendering time it consumed, represented by bar length.
// The problematic code
function Dashboard() {
const [searchTerm, setSearchTerm] = useState('');
const [items, setItems] = useState([]);
// This object gets recreated every render
const filterConfig = {
caseSensitive: false,
includeArchived: true
};
return (
<div>
<SearchBar value={searchTerm} onChange={setSearchTerm} />
<ItemList items={items} filter={filterConfig} searchTerm={searchTerm} />
</div>
);
}
function ItemList({ items, filter, searchTerm }) {
console.log('ItemList rendered'); // This keeps firing
return (
<div>
{items.map(item => (
<ItemCard key={item.id} item={item} filter={filter} />
))}
</div>
);
}
Looking at the flame graph, ItemList showed up in yellow (moderately slow), and all 100 ItemCard components underneath were gray (rendered). Just changing the search term caused all 100 cards to re-render.
But the real revelation was the "Why did this render?" feature. I clicked on ItemList and saw:
Why did this render?
- Props changed: filter
- Props changed: searchTerm
searchTerm changing made sense, but why filter? Looking back at the code, I realized filterConfig was being recreated every render. In JavaScript, {} !== {}, so React saw it as a prop change every time. This was the culprit I'd been hunting for.
Components Tab: Dissecting Your Components
Now that the Profiler found the culprit, it was time to fix it. But before fixing anything, I explored the Components tab. This showed my app's component tree, laid out like an HTML structure with the full component hierarchy.
Clicking on ItemList showed all its props, state, and hooks on the right side. Clicking the filter prop let me inspect the actual object contents. The most useful part was the "rendered by" information—I could trace exactly which parent component triggered the render.
I turned on "Highlight updates when components render" in the DevTools settings, and suddenly everything became visible. Every time I changed the search term, blue borders flashed on screen, showing exactly which parts were re-rendering in real-time. Watching all 100 cards flash convinced me: "This needs fixing."
The Optimization Workflow
With DevTools pinpointing the exact problem, fixing it was straightforward. I used a three-step approach:
Step 1: Remove unnecessary object creation
function Dashboard() {
const [searchTerm, setSearchTerm] = useState('');
const [items, setItems] = useState([]);
// Memoize the object so it's not recreated
const filterConfig = useMemo(() => ({
caseSensitive: false,
includeArchived: true
}), []); // No dependencies = created only once
return (
<div>
<SearchBar value={searchTerm} onChange={setSearchTerm} />
<ItemList items={items} filter={filterConfig} searchTerm={searchTerm} />
</div>
);
}
Step 2: Prevent unnecessary re-renders
// React.memo ensures re-render only when props actually change
const ItemCard = React.memo(({ item, filter }) => {
return (
<div className="card">
<h3>{item.title}</h3>
<p>{item.description}</p>
</div>
);
});
Step 3: Verify with Profiler
I opened the Profiler again and repeated the same interaction. This time, all 100 ItemCard components showed "Did not render" instead of gray. When the search term changed but the filter didn't, the cards simply didn't re-render. The flame graph bars were much shorter.
The perceived speed difference was dramatic. What used to take 1-2 seconds when changing filters now responded instantly. Scrolling became noticeably smoother.
Data, Not Guesswork
This experience taught me that performance optimization isn't about intuition—it's about data.
Looking at code alone, you can only guess "this part seems slow." React DevTools turned those guesses into certainty. The Profiler showed "which components are slow," the Components tab explained "why they rendered," and Highlight updates revealed "what's actually happening" in real-time.
Initially, I wanted to wrap every list component in React.memo. Classic over-optimization. But measuring with the Profiler revealed that only 2-3 components were actually problematic. The rest rendered fast enough even without memoization. Without DevTools, I would've littered the codebase with unnecessary memoization.
React DevTools is like a CT scanner for doctors. From the outside, you know "something hurts somewhere," but the scan shows exactly what's wrong. Now when I face performance issues, I open DevTools before diving into code. Five minutes of profiling saves five hours of guesswork.