April 5, 2026·8 min·By

React Performance Patterns: Stop Unnecessary Re-renders

ReactperformancememouseMemore-renders

Most React performance problems come from the same few mistakes. Here's how to diagnose and fix them systematically.

Why Components Re-render

A component re-renders when:

  1. Its state changes
  2. Its parent re-renders (even if props didn't change)
  3. Its context changes
  4. It calls a hook that triggers a re-render

Understanding this is the foundation. Everything else follows.

Profiling First

Before optimizing anything, use React DevTools Profiler:

Chrome: F12 -> React tab -> Profiler
Click Record, interact with the app, stop recording.
Look for: components with high render time or high render count.

You can also log renders during development:

function ExpensiveComponent({ data }) {
    console.count('ExpensiveComponent rendered')
    // ...
}

Only optimize what the profiler shows is actually slow.

React.memo

Prevents re-render when props haven't changed:

// Without memo: re-renders every time parent re-renders
function UserCard({ user }: { user: User }) {
    return <div>{user.name}</div>
}

// With memo: only re-renders when user prop changes
const UserCard = React.memo(function UserCard({ user }: { user: User }) {
    return <div>{user.name}</div>
})

memo uses shallow comparison. If you pass a new object reference every render, it won't help:

// Bad: new object created every render, memo does nothing
<UserCard config={{ showEmail: true }} />

// Good: stable reference
const config = useMemo(() => ({ showEmail: true }), [])
<UserCard config={config} />

useMemo

Cache expensive computations:

// Bad: recalculated every render
function ProductList({ products, filter }) {
    const filtered = products.filter(p => p.category === filter)  // Runs every render

    return filtered.map(p => <ProductCard key={p.id} product={p} />)
}

// Good: only recalculated when dependencies change
function ProductList({ products, filter }) {
    const filtered = useMemo(
        () => products.filter(p => p.category === filter),
        [products, filter]
    )

    return filtered.map(p => <ProductCard key={p.id} product={p} />)
}

Don't useMemo everything -- it adds overhead. Only memoize:

  • Computations that are measurably slow (>1ms)
  • Values passed to memo-wrapped children
  • Dependencies of useEffect

useCallback

Stable function references for event handlers passed to memoized children:

function Parent() {
    const [count, setCount] = useState(0)

    // New function reference every render -> Child always re-renders
    const handleClick = () => console.log(count)

    // Stable reference -> Child only re-renders when count changes
    const handleClick = useCallback(() => console.log(count), [count])

    return <Child onClick={handleClick} />
}

const Child = React.memo(({ onClick }) => (
    <button onClick={onClick}>Click</button>
))

useCallback is only useful when the function is passed to a memo-wrapped component or used as a useEffect dependency.

State Colocation

Move state down to where it's used:

// Bad: searchQuery state is in root, causes entire tree to re-render on every keystroke
function App() {
    const [searchQuery, setSearchQuery] = useState('')
    return (
        <div>
            <SearchBar query={searchQuery} onChange={setSearchQuery} />
            <HeavyDashboard />  // Re-renders on every keystroke!
            <Footer />          // Re-renders on every keystroke!
        </div>
    )
}

// Good: state is colocated, only SearchBar re-renders
function SearchBar() {
    const [query, setQuery] = useState('')  // State lives here
    return <input value={query} onChange={e => setQuery(e.target.value)} />
}

function App() {
    return (
        <div>
            <SearchBar />  // Only this re-renders
            <HeavyDashboard />
            <Footer />
        </div>
    )
}

Virtual Lists for Long Lists

Rendering 10,000 DOM nodes is slow. Only render what's visible:

import { FixedSizeList } from 'react-window'

function VirtualUserList({ users }: { users: User[] }) {
    return (
        <FixedSizeList
            height={600}
            itemCount={users.length}
            itemSize={72}
            width="100%"
        >
            {({ index, style }) => (
                <div style={style}>  {/* style contains position -- required */}
                    <UserCard user={users[index]} />
                </div>
            )}
        </FixedSizeList>
    )
}

For variable-height items, use VariableSizeList. For grid layouts, use react-window-infinite-loader for pagination.

The Context Trap

Context re-renders all consumers when its value changes:

// Bad: every consumer re-renders when ANY context value changes
const AppContext = createContext({ user: null, theme: 'dark', cart: [] })

// Good: split contexts by change frequency
const UserContext = createContext(null)    // Changes rarely
const ThemeContext = createContext('dark') // Changes rarely
const CartContext = createContext([])      // Changes often

If you must use one context, memoize the value:

function Provider({ children }) {
    const [user, setUser] = useState(null)
    const [cart, setCart] = useState([])

    const value = useMemo(() => ({ user, cart, setCart }), [user, cart])
    return <AppContext.Provider value={value}>{children}</AppContext.Provider>
}

Most React performance problems are solved by: profiling to find the real bottleneck, colocating state, and virtualizing long lists. memo/useMemo/useCallback are tools for specific scenarios, not defaults to apply everywhere.

K
Founder & Technical Lead, Innovibe

Building software for 15+ years. Passionate about AI, system design, and shipping things that work.

Frequently asked questions

When should I use Server Components vs Client Components?+

Server Components are the default — use them for data fetching, static content, and anything that doesn't need interactivity. Only switch to Client Components when you need browser APIs, state, effects, or event handlers. The less JS you ship to the browser, the faster your site.

Why is my Next.js build so slow?+

Usually it's TypeScript type-checking or a large dependency. Try `NEXT_TELEMETRY_DISABLED=1 next build` with `--profile` and look at what's taking time. Splitting large data files and enabling turborepo are the two biggest wins.

Should I use the App Router or Pages Router for a new project?+

App Router for new projects, full stop. Pages Router still works fine but receives fewer improvements. The learning curve for App Router is steep for one week, then you never want to go back.

Does Innovibe build this kind of thing for clients?+

Yes — this is exactly what we do day-to-day for clients across BC and Canada. If you'd rather have us build and maintain it than implement it yourself, reach out.

How do I decide whether to build this in-house or hire an agency?+

Build in-house if your team has the skills and bandwidth and this is core to your product. Hire out if it's infrastructure, if speed matters, or if the expertise gap would take months to close. We're biased, obviously — but we'll tell you honestly when in-house makes more sense.

Building something with AI?

We scope and ship AI features quickly. Let's talk.

Start a Conversation