mdashikjs/blog
All posts
React Performance: The Optimization Checklist I Actually Use
Frontend

React Performance: The Optimization Checklist I Actually Use

Frontend5 min

React Performance: The Optimization Checklist I Actually Use

I have seen too many React apps slow down because of premature optimization or, worse, no optimization at all. Here is the checklist I follow when a component feels sluggish.

ReactPerformanceOptimizationTypeScript
Share:

Step 0: Measure first

Do not optimize what you have not profiled. Open React DevTools Profiler, reproduce the slow interaction, and look at what is re-rendering and how long each render takes.

1. Eliminate unnecessary re-renders

The most common issue. A parent re-renders and all children re-render even if their props did not change.

// Before: re-renders on every parent render
function ExpensiveList({ items }: { items: Item[] }) {
  return items.map(item => <ExpensiveRow key={item.id} item={item} />);
}

// After: only re-renders when items actually change
const ExpensiveList = memo(function ExpensiveList({ items }: { items: Item[] }) {
  return items.map(item => <ExpensiveRow key={item.id} item={item} />);
});

But memo only works if the props are referentially stable. If you pass a new object or array every render, memo is useless.

2. Stabilize callbacks and objects

// Bad: new function on every render, breaks memo on children
<Button onClick={() => handleClick(id)} />

// Good: stable reference
const handleButtonClick = useCallback(() => handleClick(id), [id]);
<Button onClick={handleButtonClick} />

Same for objects passed as props — use useMemo to keep the reference stable.

3. Move state down

If a piece of state only affects one subtree, move it into that subtree. A text input's value does not need to live in the page-level component.

4. Virtualize long lists

Rendering 1,000 DOM nodes when only 20 are visible is pure waste. Use @tanstack/react-virtual:

const virtualizer = useVirtualizer({
  count: items.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 50,
});

5. Code-split heavy components

const HeavyChart = lazy(() => import('./HeavyChart'));

<Suspense fallback={<ChartSkeleton />}>
  <HeavyChart data={data} />
</Suspense>

The chart library is only loaded when the component is rendered. Initial page load is faster.

6. Debounce expensive computations

Search inputs, filter functions, and any computation triggered by user typing should be debounced. 150ms is usually the right delay — fast enough to feel responsive, slow enough to batch keystrokes.

When to stop optimizing

When the Profiler shows render times under 16ms (one frame at 60fps). Below that threshold, further optimization is invisible to the user and adds complexity to the code.

MA

Written by Md Ashik

Senior Software Engineer building reliable backends. I write about the practical tradeoffs behind shipping software that holds up in production.