Ayush Pandey
Back to Blog

React Performance Optimization Techniques

12 min read
reactperformancefrontend

React Performance Optimization Techniques

Fast apps are not built by accident. They are built by making the right tradeoffs early, measuring the right things, and fixing the bottlenecks that actually matter.

When a React app feels slow, the cause is usually not one big problem. It is a chain of small ones: unnecessary re-renders, oversized bundles, expensive list rendering, bad memoization, and work that happens too often on the main thread.

This guide covers practical techniques you can use to make React apps faster without making the codebase unreadable.

Why Performance Matters

React is fast by default, but it does not protect you from bad component boundaries or expensive render trees. As a product grows, performance issues start to show up in places users notice immediately:

  • typing feels laggy
  • navigation takes longer than expected
  • lists stutter while scrolling
  • buttons feel delayed
  • pages become heavy on mobile devices

Performance is not just about benchmarks. It is about reducing friction in the product experience.

1. Measure Before You Optimize

The first rule of optimization is simple: do not guess.

Use these tools before changing code:

  • React DevTools Profiler
  • Chrome Performance tab
  • Lighthouse
  • Web Vitals in production

Look for:

  • components that render too often
  • renders that take too long
  • large bundles
  • slow interactions like search, filtering, or drag-and-drop

Example: Profiling a slow component

import { Profiler } from 'react';

function onRender(
  id: string,
  phase: 'mount' | 'update',
  actualDuration: number
) {
  console.log({ id, phase, actualDuration });
}

export function App() {
  return (
    <Profiler id="Dashboard" onRender={onRender}>
      <Dashboard />
    </Profiler>
  );
}

If a component is slow, find out why it is slow before trying to memoize everything.

2. Prevent Unnecessary Re-renders

React re-renders when state or props change. That is normal. The problem starts when a change in one area causes a large tree to update unnecessarily.

Common causes

  • passing new object literals on every render
  • creating inline functions inside deeply nested trees
  • lifting state too high
  • storing derived values in state instead of computing them

Better pattern

function UserCard({ user, onSelect }) {
  return (
    <button onClick={() => onSelect(user.id)}>
      {user.name}
    </button>
  );
}

This looks harmless, but in a large list it can create a lot of churn.

Prefer stable props where possible:

import { memo, useCallback } from 'react';

const UserCard = memo(function UserCard({ user, onSelect }) {
  return <button onClick={() => onSelect(user.id)}>{user.name}</button>;
});

export function UserList({ users }) {
  const handleSelect = useCallback((id: string) => {
    console.log('Selected:', id);
  }, []);

  return users.map((user) => (
    <UserCard key={user.id} user={user} onSelect={handleSelect} />
  ));
}

Use React.memo only when there is a measurable problem. Do not wrap every component by default.

3. Split Heavy Code With Dynamic Imports

If a route ships a large chart library, code editor, or analytics panel on initial load, users pay for code they may never use.

Use dynamic imports for heavy, route-specific features.

import dynamic from 'next/dynamic';

const AnalyticsPanel = dynamic(() => import('./AnalyticsPanel'), {
  loading: () => <p>Loading analytics...</p>,
});

export function DashboardPage() {
  return <AnalyticsPanel />;
}

This reduces the initial JavaScript payload and improves first load time.

4. Memoize Expensive Computations

Do not store derived values in state unless you truly need to.

If a value can be computed from props or existing state, compute it during render. If the computation is expensive, memoize it.

import { useMemo } from 'react';

export function ProductList({ products, query }) {
  const filteredProducts = useMemo(() => {
    const q = query.toLowerCase();
    return products.filter((product) =>
      product.name.toLowerCase().includes(q)
    );
  }, [products, query]);

  return filteredProducts.map((product) => (
    <div key={product.id}>{product.name}</div>
  ));
}

Use useMemo for expensive work, not as a default habit.

5. Virtualize Long Lists

Rendering hundreds or thousands of DOM nodes at once hurts performance. The browser has to layout, paint, and maintain them even when they are off screen.

Virtualization keeps only visible rows in the DOM.

Good candidates

  • chat messages
  • log viewers
  • search results
  • dashboards with large tables
  • feeds and activity streams

Concept

If you need a library, react-window is a good lightweight option.

6. Keep State Local

State should live as close as possible to the component that actually needs it.

If you lift too much state to the top, every change forces large sections of the UI to re-render.

Example

Bad:

  • one parent stores all form state
  • every keystroke re-renders the whole page

Better:

  • each form section owns its own state
  • only the affected section updates

This is one of the simplest ways to reduce render cost in React apps.

7. Avoid Expensive Work During Render

Rendering should stay cheap.

Avoid:

  • parsing large data structures in render
  • doing sorting inside JSX
  • expensive date formatting in large lists
  • creating large nested arrays from scratch on every render

Move expensive work into memoized selectors, server-side processing, or precomputed values.

Example

const sortedItems = useMemo(() => {
  return [...items].sort((a, b) => a.rank - b.rank);
}, [items]);

8. Use Concurrent Features Thoughtfully

React 18+ gives you tools to keep the UI responsive while expensive updates happen.

useDeferredValue

Useful when a search input updates a heavy results list.

import { useDeferredValue } from 'react';

export function SearchResults({ query, results }) {
  const deferredQuery = useDeferredValue(query);

  const filtered = results.filter((item) =>
    item.name.toLowerCase().includes(deferredQuery.toLowerCase())
  );

  return filtered.map((item) => <div key={item.id}>{item.name}</div>);
}

startTransition

Useful when an update is important but not urgent.

import { startTransition, useState } from 'react';

export function SearchBox({ onSearch }) {
  const [value, setValue] = useState('');

  const handleChange = (nextValue: string) => {
    setValue(nextValue);
    startTransition(() => {
      onSearch(nextValue);
    });
  };

  return <input value={value} onChange={(e) => handleChange(e.target.value)} />;
}

These APIs improve perceived responsiveness, especially on slower devices.

9. Optimize Images and Media

A lot of performance problems are not caused by React at all. Images are often the real issue.

Use:

  • properly sized images
  • modern formats where possible
  • lazy loading for below-the-fold assets
  • responsive image sizes
  • optimized video embeds

In Next.js, the Image component already gives you a solid starting point.

10. Reduce Bundle Size

Large bundles slow down parse time, execution time, and hydration.

To reduce bundle size:

  • remove unused dependencies
  • split vendor-heavy features
  • prefer smaller libraries when possible
  • inspect bundle output regularly

If a dependency is only used in one route, do not ship it everywhere.

11. Be Careful With Context

React Context is useful, but it can also cause broad re-renders if a single value changes often.

If the provider updates frequently, every consumer may re-render.

Use context for:

  • theme
  • auth state
  • global layout data

Avoid using it as a replacement for local component state or high-frequency UI state.

12. Cache Where It Makes Sense

Caching can help on multiple levels:

  • server responses
  • expensive database queries
  • computed data
  • client-side fetched results

In Next.js, server caching and route-level caching can significantly reduce repeated work.

The goal is not to cache everything. The goal is to avoid repeating the same expensive work when the result has not changed.

13. Track Real User Performance

Lab results are useful, but real users matter more.

Track:

  • Core Web Vitals
  • interaction delay
  • route transition time
  • slow API calls
  • errors that affect rendering

Performance regressions are easier to fix when you catch them early.

A Simple Optimization Workflow

The best optimization workflow is boring:

  1. reproduce the issue
  2. measure it
  3. fix the real bottleneck
  4. verify the improvement
  5. keep the code maintainable

Final Thoughts

React performance is mostly about discipline:

  • keep rendering cheap
  • keep state local
  • load less code
  • compute less during render
  • measure before and after

The fastest app is not the one with the most memoization. It is the one that avoids doing unnecessary work in the first place.

More Articles

View All Articles