All Posts
ReactPerformance
Mar 25, 20269 min read

The React Pattern That's Silently Killing Your App's Performance

There's one pattern I see in almost every production React codebase I audit. It causes invisible re-renders on every keystroke, doubles your render count, and nobody talks about it.

The React Pattern That's Silently Killing Your App's Performance

Last quarter I audited the frontend codebase at three different companies — a Series A startup, a mid-size SaaS, and a late-stage fintech. All three had the same problem. And none of their teams knew it was there.

It caused anywhere from 40% to 300% unnecessary re-renders depending on the component tree. On one app, fixing it dropped the CPU usage on their dashboard page from 60ms to 11ms per interaction. Here's what it is, and why it's so hard to see.

The Pattern

Inline object and array creation inside the render function, passed as props. It looks like this:

// This looks fine. It is not.

<DataTable

columns={[

{ key: "name", label: "Name" },

{ key: "email", label: "Email" },

]}

options={{ sortable: true, selectable: false }}

onSort={(col) => handleSort(col)}

/>

Every single render of the parent component creates brand-new array and object references for columns and options. React's reconciliation uses Object.is() to compare props. Two different object references are never equal under Object.is(), even if their values are identical.

This means React.memo on your DataTable is completely useless. The component will re-render every time the parent re-renders — which, in a form with controlled inputs, is every single keystroke.

Why React.memo Doesn't Save You

I see teams add React.memo to their expensive components and then pat themselves on the back. But React.memo does a shallow comparison of every prop. If any prop is a new reference, it re-renders. Period.

The fix isn't memoization alone. The fix is stable references:

// ✅ Option 1: define outside the component (static data)

const COLUMNS = [

{ key: "name", label: "Name" },

{ key: "email", label: "Email" },

];


// ✅ Option 2: useMemo (dynamic data)

const columns = useMemo(() => [

{ key: "name", label: user.preferredName },

]), [user.preferredName]);

The rule: anything you pass as a prop to a memoized component — objects, arrays, functions — must have a stable reference. If it's static, define it outside. If it's dynamic, memoize it.

The Arrow Function Trap

The same problem applies to inline arrow functions. onSort={(col) => handleSort(col)} creates a new function on every render. Wrap it in useCallback, or just pass handleSort directly if it doesn't need a wrapper.

But here's where I'll push back on the common wisdom: don't useMemo everything. Memoization has a cost. For cheap components that legitimately need to re-render often, you're adding overhead for no gain. Profile first. Memoize second.

How to Find This in Your Codebase Today

Install React DevTools. Enable “Highlight updates when components render.” Then type in a form. Watch which components flash. If components that have nothing to do with your input are lighting up, you have this problem.

For a deeper audit, use the Profiler tab. Record an interaction, then look for components that rendered “because the parent re-rendered” instead of “because state changed.” Those are your targets.

One codebase I audited had a search input that caused 47 distinct component re-renders per keystroke. After fixing inline props and stabilizing function references, it was down to 6. The UI felt completely different — not faster in a benchmark, but faster in the way a user notices without knowing why.

Built with Passion

© 2026 Built with ❤️ & Code by Nishal Poojary.

The Land of Spirituality and Philosophy

Bangalore · India

Thanks for making it
to the end 🙌🏻

Footer panoramic mountain landscape graphic