Optimizing React Performance: Tips and Tricks

November 5, 20256 min read
ReactPerformanceOptimization

So here's the thing - I used to think React was slow. Turns out, I was just using it wrong. Let me share the performance mistakes I made (so you don't have to) and what actually worked to speed things up.

When I Realized I Had a Problem

I built this dashboard with a ton of data tables. It was smooth at first, but as I added features, it became sluggish. Every keystroke in a filter input felt delayed. Users noticed. I needed to fix it.

Understanding Why React Re-renders

First, I had to understand when React re-renders components:

1. When state changes (obvious)

2. When props change (also obvious)

3. When a parent re-renders (wait, what? yeah, this one got me)

4. When context value changes (this killed my performance once)

Fixes That Actually Worked

1. React.memo Was My First Win

I had this expensive component that showed user stats. It re-rendered every time anything changed, even unrelated stuff. React.memo fixed it:

typescript
import { memo } from 'react';

const UserStatsCard = memo(({ userId, stats }) => {
  // Complex calculations and charts
  return <div>{/* render stats */}</div>;
});

Now it only re-renders when userId or stats actually change. Simple fix, massive impact.

2. useMemo Saved My Sorting Logic

I had a list of 1000+ items that I sorted on every render. Even when the list didn't change. Yeah, not smart:

typescript
// ❌ Before: Sorting on every render
function DataTable({ items }) {
  const sortedItems = items.sort((a, b) => a.value - b.value);
  return <Table data={sortedItems} />;
}

// ✅ After: Sort only when items change
function DataTable({ items }) {
  const sortedItems = useMemo(() => {
    return items.sort((a, b) => a.value - b.value);
  }, [items]);
  
  return <Table data={sortedItems} />;
}

The difference was instant. Scrolling became butter-smooth.

3. useCallback for Stable Function References

This one's subtle but important. I was passing callbacks to child components, and they kept re-rendering:

typescript
function Parent() {
  // ❌ New function every render
  const handleClick = () => console.log('clicked');
  
  return <Child onClick={handleClick} />; // Child re-renders every time
}

// ✅ Stable function reference
function Parent() {
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []); // Only created once
  
  return <Child onClick={handleClick} />; // Child doesn't re-render unnecessarily
}

Combine this with React.memo on the child, and you've got a solid optimization.

4. Code Splitting Changed Everything

My initial bundle was huge - like 3MB huge. Users on slow connections waited forever. Code splitting with lazy loading fixed it:

typescript
import { lazy, Suspense } from 'react';

const AdminPanel = lazy(() => import('./AdminPanel'));
const Charts = lazy(() => import('./Charts'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      {isAdmin && <AdminPanel />}
      <Charts />
    </Suspense>
  );
}

Now users only download what they need. Initial load went from 5 seconds to under 1 second.

5. Virtualization for Long Lists

I had a list with 10,000 items. Rendering all of them? My browser literally froze. React-window saved me:

typescript
import { FixedSizeList } from 'react-window';

function HugeList({ items }) {
  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>
          {items[index].name}
        </div>
      )}
    </FixedSizeList>
  );
}

It only renders what's visible on screen. Genius. Performance went from unusable to perfect.

State Management Mistakes I Made

Lifting State Too High

I used to put everything in a top-level state. Bad idea. If a button in the footer needs state, don't put it in App.tsx:

typescript
// ❌ State way too high
function App() {
  const [modalOpen, setModalOpen] = useState(false); // Used only in Footer
  return <><Header /><Content /><Footer modalOpen={modalOpen} setModalOpen={setModalOpen} /></>;
}

// ✅ State where it's actually used
function Footer() {
  const [modalOpen, setModalOpen] = useState(false);
  return <div>{/* use modalOpen locally */}</div>;
}

Keep state close to where it's used. My unnecessary re-renders dropped by 80%.

The Context Trap

I put user data and theme settings in one context. Changing theme? Entire app re-renders because user context changed too. Split them:

typescript
// ❌ Everything in one context
const AppContext = createContext({ user, theme, setUser, setTheme });

// ✅ Separate concerns
const UserContext = createContext({ user, setUser });
const ThemeContext = createContext({ theme, setTheme });

Now changing theme doesn't cause user-related components to re-render. Big win.

Tools That Helped Me Debug

React DevTools Profiler

This tool is criminally underused. It shows you:

- Which components are slow

- How often they re-render

- What's causing the re-renders

Here's how I use it:

1. Open React DevTools

2. Go to Profiler tab

3. Hit Record

4. Use my app normally

5. Stop recording and analyze

I found components re-rendering 50+ times per second. Fixed those first.

Mistakes I See Everyone Make

Creating Objects/Arrays Inline

typescript
// ❌ New object every render
<Component style={{ padding: 10 }} />

// ✅ Reuse the same object
const style = { padding: 10 };
<Component style={style} />

Anonymous Functions Everywhere

typescript
// ❌ New function every render
<Button onClick={() => handleClick(id)} />

// ✅ Memoize it
const onClick = useCallback(() => handleClick(id), [id]);
<Button onClick={onClick} />

The Most Important Lesson

Don't optimize prematurely. Seriously. I wasted weeks over-optimizing a form that was already fast.

My process now:

1. Build the feature

2. Test it

3. If it's slow, profile it

4. Optimize the actual bottleneck

5. Test again

Most of the time, React is fast enough out of the box. Only optimize when you have a real problem.

Quick Wins Checklist

- [ ] Use React.memo for expensive components

- [ ] Memoize expensive calculations with useMemo

- [ ] Stabilize callbacks with useCallback

- [ ] Code-split heavy features

- [ ] Virtualize long lists

- [ ] Keep state local

- [ ] Split contexts by concern

- [ ] Profile before optimizing

Final Thoughts

React performance isn't rocket science. It's about understanding when and why things re-render, then being strategic about preventing unnecessary work.

Start with the big wins (code splitting, virtualization). Then profile to find real bottlenecks. Don't waste time optimizing things that are already fast enough.

And remember: a working, slightly slow app beats a perfectly optimized app that doesn't exist. Ship first, optimize later.

Good luck! ⚡