Real-world performance tips for React apps (beyond memo and useCallback)

Real-world performance tips for React apps (beyond memo and useCallback)

Neji

Hey devs,

You already know about React.memo and useCallback, right? Yeah, it’s nice to know them, but honestly… putting them everywhere won’t always make your app faster. Sometimes, it can even slow things down.

So in this post, I’ll go through some performance tricks I actually used when working on real React apps – not just theory stuff. Everything I share here comes from what I faced in projects, not just copy from tutorials.

Let’s go!


1. Break down big components (and care about what causes re-render)

Sometimes you put too much stuff in one big component (I did that too).
The issue is: when your state updates, React will re-render the full component, even if only one small piece needs it.

Try to separate concerns like this:

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

  return (
    <>
      <Header />
      <MainContent />
      <Footer />
      <button onClick={() => setCount(count + 1)}>Click</button>
    </>
  );
}

So when count updates, only the piece of UI that uses it will be affected.
(If needed, you can wrap it with React.memo to avoid extra re-renders)


2. Lazy load heavy components (charts, modals, maps, etc.)

Let’s say you have a chart, a map, or some modal that not always show. No need to load it from the beginning. Use lazy load:

const HeavyComponent = React.lazy(() => import('./HeavyComponent'));

function Page() {
  const [show, setShow] = useState(false);

  return (
    <>
      <button onClick={() => setShow(true)}>Show details</button>
      {show && (
        <Suspense fallback={<div>Loading...</div>}>
          <HeavyComponent />
        </Suspense>
      )}
    </>
  );
}

This helps reduce initial bundle size a lot.


3. Avoid too many inline functions in JSX

Inline anonymous functions are created on every render:

<button onClick={() => doSomething()}>Click</button>

This will create new function every render. If you pass it to child component, it can cause unnecessary re-renders.

Better way:

const handleClick = useCallback(() => {
  doSomething();
}, []);

<button onClick={handleClick}>Click</button>

4. Use stable keys in lists (avoid index as key)

Using index as key is okay for simple case, but not good when list can be reordered or updated.

Bad:

{items.map((item, i) => (
  <Item key={i} data={item} />
))}

Better:

{items.map((item) => (
  <Item key={item.id} data={item} />
))}

Using the wrong key often leads to strange behavior and can slow things down.


5. Debounce search, input, or any expensive logic

If your app calls an API or runs logic every time user types a letter, that’s gonna hurt performance.

You could use lodash.debounce, or write a custom useDebounce hook for better control:

const debouncedValue = useDebounce(value, 300);

This makes input smoother and avoids flooding your server.


6. Be careful with context and global state re-renders

Context is good, but if you put too much in one context, every component using it will re-render when it changes.

What I do:

  • Split context into smaller ones (AuthContext, ThemeContext, etc.)
  • For global state, sometimes use Zustand or Redux Toolkit with selector

7. Use Chrome DevTools and React DevTools to measure

Performance is not just feeling. Use tools:

  • Chrome DevTools → Performance tab
  • React DevTools → Profiler tab

Try to open Profiler and click around your app. You’ll probably find something surprising.


Final thoughts

Performance in React is not just about memo or useCallback. It’s about how you design your component, how you manage state, and how you split logic smartly.

Keep your code easy to read first. Then when things feel slow, that’s when you optimize.

Hope these tips help your real project, not just demo app.

Thanks for reading, I’ll share more on frontend stuff soon!