How I Caught a Silent Performance Killer in Code Review
A common react anti-pattern that disables memoization.
A few days ago I was reviewing a PR for a dashboard expected to handle tens of thousands of rows. On the surface everything looked correct — TypeScript, TanStack Table, react-window. Then this slipped through:
const VirtualizedTable = ({ rows }: { rows: Row[] }) => {
const RowComponent = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style} className="table-row">
{rows[index].name} · {rows[index].status}
</div>
);
return (
<FixedSizeList height={800} itemCount={rows.length} itemSize={48} width="100%">
{RowComponent}
</FixedSizeList>
);
};
I asked the author to move the renderer out of the component. They pushed back: “It works fine on my machine. Why bother?”
Because this single decision silently turns off one of React’s most important performance mechanisms.
React’s core performance principle that everyone forgets
React’s rendering speed doesn’t come from diffing deeply or cleverly traversing trees. It comes from something simpler:
React avoids work whenever possible. Specifically:
If the element type changes, React skips diffing and remounts the subtree.
If the type stays stable and props are shallow-equal, the subtree is reused.
Libraries like react-window rely heavily on this: they memoize the row renderer so each visible cell doesn’t rerender on every scroll tick.
By defining RowComponent inside VirtualizedTable, the component type changes on every re-render, which forces React to throw away the subtree and bypass memoization.
That’s why this “clean” pattern destroys performance at scale.
Measured impact at ~20,000 rows
| Scenario | Avg FPS | CPU | Memory |
|---|---|---|---|
| Inline renderer | 18–24 | ~85 % | 420 MB |
| External renderer + memo | 58–60 | ~22 % | 48 MB |
This isn’t micro-optimization. It’s the difference between “native smooth” and “please kill this tab.”
The corrected version I approved
const RowComponent = memo(
({ data, index, style }: { data: Row[]; index: number; style: React.CSSProperties }) => (
<div style={style} className="table-row">
{data[index].name} · {data[index].status}
</div>
)
);
const VirtualizedTable = ({ rows }: { rows: Row[] }) => (
<FixedSizeList
height={800}
itemCount={rows.length}
itemSize={48}
itemData={rows}
width="100%"
>
{RowComponent}
</FixedSizeList>
);
The lag disappeared immediately.
Why virtualization only works if you let it work
Despite all the hype around “React is slow,” the truth is simpler:
React is extremely fast as long as you don’t disable its heuristics.
react-window gives you constant-time rendering because it displays only a small visible window of rows. But it assumes:
- row component identity is stable
- props are shallow and memoizable
- data is passed through itemData and not captured
Break one of these and you’re back to pushing tens of thousands of elements through the reconciliation pipeline.
How React actually compares to other frameworks in 2025
I’ve benchmarked similar large-table scenarios across frameworks:
Svelte — compiles to minimal DOM operations; extraordinarily fast.
Solid.js — fine-grained reactivity avoids wasted renders; top-tier for massive lists.
Vue 3 — with virtual scrolling, performance is nearly identical to optimized React.
Angular — predictable and solid, but heavier.
React — not the fastest raw DOM performer, but extremely competitive when you respect its rules.
React’s advantage remains the ecosystem: TanStack Table, react-window, AG Grid, MUI X — all industrial-grade, all battle-tested at scale.
When you combine those tools with proper component identity management, React comfortably handles six-figure row counts.
My 2025 checklist for any table or feed that might scale
-
Renderer defined at module scope, memoized
-
Row data passed through itemData, never closed over
-
Stable itemKey for reorderable rows
-
Validate flamegraph: frames <16 ms under load
-
Test with a mid-tier Android device, not a dev machine
-
For huge feature sets: AG Grid; for flexibility: TanStack Table + react-window
Why this is now an automatic “needs changes” in my reviews
This mistake is invisible in development and catastrophic in production. Even senior engineers miss it because React doesn’t warn you, and the code looks clean.
But the cost of ignoring it is weeks of debugging “mysterious” performance issues.
Detect it early, fix it immediately, and React remains a top performer — not because it’s magically fast, but because its heuristics are built on the assumption that you give the library stable component identities to optimize.