Sitemap
Salim Ansari

Salim Ansari

Frontend Engineer with 3+ years of experience at Network18, building Firstpost & CNBCTV18.com. Sharing JavaScript, React & frontend insights.

React Performance Optimization: A Practical Guide

9 min readSep 13, 2026

--

Press enter or click to view image in full size

Performance is one of the most important aspects of building production-grade React applications.

When an application becomes slow, developers often immediately reach for:

  1. useMemo()
  2. useCallback()
  3. React.memo()

But React performance optimization is much broader than memoization.

A better approach is to understand where the work is happening, measure the bottleneck, and then optimize the right layer.

In this article, we’ll explore the most important React performance optimization techniques with practical examples.

1. What Does React Performance Mean?

A simplified React rendering pipeline looks like this:

State / Props / Context Change
↓
React Render
↓
Reconciliation
↓
Commit Phase
↓
DOM Updates
↓
Browser Layout / Paint
↓
UI

Performance optimization means reducing unnecessary or expensive work in this pipeline.

There are several common sources of performance problems:

Unnecessary re-renders
+
Expensive calculations
+
Large component trees
+
Large JavaScript bundles
+
Huge lists
+
Too many API requests
+
Large images
+
Excessive browser work

2. Measure Before Optimizing

The first rule of performance optimization is:

Don’t optimize based on assumptions. Measure first.

Useful tools include:

  • React DevTools Profiler
  • Chrome Performance panel
  • Lighthouse
  • Network tab
  • Bundle analyzers
  • Core Web Vitals

A good optimization workflow is:

Identify problem
↓
Measure
↓
Find bottleneck
↓
Optimize
↓
Measure again

For example, if a dashboard feels slow, first determine whether the problem is:

  1. React rendering?
  2. JavaScript execution?
  3. Network?
  4. Large images?
  5. Large DOM?
  6. Expensive calculation?

Only then choose the optimization technique.

3. Understand Re-renders

Consider:

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

return (
<>
<button onClick={() => setCount(count + 1)}>
{count}
</button>

<Profile />
</>
);
}

When count changes, App renders again.

The important thing to understand is:

A React re-render does not necessarily mean the DOM will be updated.

React can render, compare the result, and determine that some DOM doesn’t need to change.

So: Re-render ≠ DOM update

However, the render work itself can still be expensive.

4. Keep State as Local as Possible

One of the simplest and most effective optimization techniques is state colocation.

Consider:

function App() {
const [search, setSearch] = useState("");

return (
<>
<Navbar />
<Search value={search} onChange={setSearch} />
<HugeDashboard />
<Footer />
</>
);
}

Every search update causes App to render again.

If the state is only required by the search section, move it closer to where it’s used:

function SearchSection() {
const [search, setSearch] = useState("");

return (
<>
<Search
value={search}
onChange={setSearch}
/>

<SearchResults search={search} />
</>
);
}

function App() {
return (
<>
<Navbar />
<SearchSection />
<HugeDashboard />
<Footer />
</>
);
}

Now the search state update is isolated to a smaller subtree.

Principle

Keep state as close as possible to the components that actually need it.

Don’t move state to the top of the application unless it genuinely needs to be shared.

5. React.memo

React.memo allows React to skip rendering a component when its props haven’t changed.

const UserCard = React.memo(function UserCard({ user }) {
return (
<div>
{user.name}
</div>
);
});

Suppose the parent renders again:

Parent renders
↓
UserCard receives same props
↓
React.memo comparison
↓
Skip unnecessary render

This can be useful when:

  • The child is expensive to render
  • The child renders frequently
  • Props remain stable
  • The parent re-renders frequently

6. Referential Equality Matters

Consider:

function App() {
const user = {
name: "Salim"
};

return <UserCard user={user} />;
}

Every render creates a new object:

Render 1 → Object A
Render 2 → Object B
Render 3 → Object C

Even though the values are identical:

ObjectA === ObjectB // false

Therefore, React.memo may not be able to skip the render.

This is because JavaScript compares objects and functions by reference.

For example:

{} === {} // false

[] === [] // false

7. useMemo

useMemo caches the result of a calculation.

Consider:

const filteredProducts = useMemo(() => {
return products.filter(product =>
product.name
.toLowerCase()
.includes(search.toLowerCase())
);
}, [products, search]);

Now the filtering calculation runs when:

products changes OR search changes

If some unrelated state changes:

count changes
↓
Component renders
↓
products unchanged
search unchanged
↓
Reuse memoized result

8. When Should You Use useMemo?

useMemo is useful when:

  • A calculation is expensive
  • A large dataset is being processed
  • You need a stable object reference
  • You need stable derived data for a memoized child

Example:

const sortedProducts = useMemo(() => {
return [...products].sort((a, b) =>
a.price - b.price
);
}, [products]);

But don’t use useMemo for every calculation.

Avoid unnecessary code like:

const fullName = useMemo(
() => `${firstName} ${lastName}`,
[firstName, lastName]
);

Rule

Use useMemo when the work or referential stability justifies the cost.

9. useCallback

useCallback memoizes a function reference.

Consider:

const ProductList = React.memo(function ProductList({
onDelete
}) {
// ...
});

If the parent does this:

function App() {
const handleDelete = (id) => {
deleteProduct(id);
};

return (
<ProductList onDelete={handleDelete} />
);
}

a new function is created whenever App renders.

Therefore: oldFunction !== newFunction

ProductList may re-render even though its behavior hasn’t changed.

You can stabilize the reference:

const handleDelete = useCallback((id) => {
deleteProduct(id);
}, []);

Now the function reference remains stable as long as its dependencies remain unchanged.

10. Virtualization for Large Lists

Imagine rendering: 100,000 users

with:

users.map(user => (
<UserRow
key={user.id}
user={user}
/>
))

This can create a huge DOM tree.

Instead, use virtualization.

The idea is simple:

Render only the items currently visible in the viewport.

For example:

100,000 rows

────────────────────
Visible rows:

501
502
503
...
520

────────────────────

Instead of rendering all 100,000 DOM nodes, virtualization renders only a small window of visible items.

Libraries such as react-window and @tanstack/react-virtual can help implement this.

Best use cases

  • Large tables
  • Chat messages
  • Long feeds
  • File explorers
  • Search results
  • Large dropdowns

11. Code Splitting

Large JavaScript bundles can slow down initial page loading.

Instead of:

main.js
├── Home
├── Dashboard
├── Analytics
├── Admin
├── Checkout
└── Editor

you can split the application into smaller chunks:

main.js
home.js
dashboard.js
analytics.js
admin.js
checkout.js

This is called code splitting.

12. Lazy Loading

React provides lazy() for component-level lazy loading:

const Analytics = lazy(
() => import("./Analytics")
);

Then:

<Suspense fallback={<AnalyticsSkeleton />}>
<Analytics />
</Suspense>

Now Analytics doesn’t need to be part of the initial critical JavaScript path.

Conceptually:

Initial page
↓
Small initial bundle
↓
User opens Analytics
↓
Analytics chunk downloaded
↓
Skeleton shown
↓
Analytics rendered

Good candidates for lazy loading include:

  • Admin dashboards
  • Analytics
  • Rich text editors
  • Large charts
  • Maps
  • Payment interfaces
  • Rarely visited routes

13. Don’t Lazy-Load Everything

This isn’t a good idea:

const Button = lazy(() => import("./Button"));

For a tiny component, the cost of introducing another asynchronous chunk can outweigh the benefit.

Get Salim Ansari’s stories in your inbox

Join Medium for free to get updates from this writer.

A good rule is:

Lazy-load large or infrequently used features, not every component.

14. Debounce Expensive Search

Consider a search input:

<input onChange={handleSearch} />

If the user types:

r
re
rea
reac
react

you could potentially trigger five API requests.

Instead, debounce the operation:

const handleSearch = debounce((value) => {
fetchResults(value);
}, 300);

Now:

User types
r
re
rea
reac
react
↓
Wait 300ms
↓
API request

This reduces:

  • API calls
  • Network traffic
  • Server work
  • Client-side processing

15. Debounce + AbortController

For search experiences, debounce can be combined with AbortController.

User searches "react"
↓
Request A starts
↓
User searches "react hooks"
↓
Abort Request A
↓
Request B starts
↓
Only latest search matters

This prevents stale requests from continuing unnecessarily.

The important principle is:

Don’t just optimize the request; make the entire interaction cancellation-safe.

That includes:

  • Request
  • Loading state
  • Response handling
  • Retries
  • UI updates

16. Avoid Unnecessary useEffect

A common performance mistake is using an effect for something that can be calculated directly.

Bad:

const [fullName, setFullName] = useState("");

useEffect(() => {
setFullName(
`${firstName} ${lastName}`
);
}, [firstName, lastName]);

This creates an additional state update:

Render
↓
Effect
↓
setState
↓
Another render

Instead:

const fullName = `${firstName} ${lastName}`;

Now there is no unnecessary state update.

Rule

If a value can be derived during rendering, don’t automatically store it in state.

17. Context API and Performance

Context can also cause broad updates.

Consider:

const AppContext = createContext();

function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState("dark");

const value = {
user,
setUser,
theme,
setTheme,
};

return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
}

The provider value is a new object whenever the provider renders.

You can stabilize it when appropriate:

const value = useMemo(() => ({
user,
setUser,
theme,
setTheme,
}), [user, theme]);

But there’s a deeper issue.

If one context contains:

User
Theme
Cart
Notifications
Settings

an update to one concern can affect many consumers.

For larger applications, consider splitting contexts:

<UserProvider>
<ThemeProvider>
<CartProvider>
<App />
</CartProvider>
</ThemeProvider>
</UserProvider>

18. Optimize Images

React isn’t the only source of performance problems.

Large images can significantly increase page weight.

Instead of blindly loading huge images:

<img src="/huge-product-image.jpg" />

use appropriately sized and optimized images.

In Next.js:

<Image
src="/product.jpg"
alt="Product"
width={800}
height={600}
/>

Image optimization can improve:

  • Loading time
  • LCP
  • Network usage
  • Mobile performance

For image-heavy applications, this can have a larger impact than micro-optimizing React renders.

19. Stable Keys

When rendering lists, use stable and unique keys.

Avoid:

users.map((user, index) => (
<User
key={index}
user={user}
/>
))

Prefer:

users.map(user => (
<User
key={user.id}
user={user}
/>
))

Keys help React understand the identity of list items during reconciliation.

With stable keys, React can better determine:

. Was an item added?
. Was it removed?
. Did it move?
. Did it change?

Using array indexes can cause problems when items are inserted, deleted, or reordered.

20. Component Composition

Sometimes the best optimization isn’t React.memo.

Suppose:

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

return (
<>
<Counter
count={count}
onIncrement={() =>
setCount(c => c + 1)
}
/>

<ExpensiveComponent />
</>
);
}

If ExpensiveComponent doesn’t depend on count, you can structure the component tree so the state lives lower:

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

return (
<Counter
count={count}
onIncrement={() =>
setCount(c => c + 1)
}
/>
);
}

function App() {
return (
<>
<CounterSection />
<ExpensiveComponent />
</>
);
}

Now the state update is localized.

This is another example of:

Good component architecture can be a performance optimization.

21. Server-Side Rendering and React Performance

For frameworks such as Next.js, performance also involves architectural decisions such as:

SSR
SSG
ISR
Streaming
Server Components
Client Components

For example, Server Components can reduce the amount of JavaScript that needs to be sent to the browser for components that don’t require client-side interactivity.

Similarly, static generation can avoid repeating server work for content that doesn’t need to be generated per request.

The correct strategy depends on the application’s requirements.

22. Core Web Vitals

Ultimately, React performance should be measured from the user’s perspective.

Three important Web Vitals are:

LCP — Largest Contentful Paint

Measures how quickly the main content becomes visible.

Slow server/image
↓
Slow LCP

INP — Interaction to Next Paint

Measures responsiveness to user interactions.

Click
↓
Long JavaScript task
↓
Delayed UI response
↓
Poor INP

CLS — Cumulative Layout Shift

Measures unexpected layout movement.

Image loads
↓
No reserved dimensions
↓
Content moves
↓
Poor CLS

Therefore, React performance optimization should ultimately improve real user experience, not just reduce the number shown in the React Profiler.

A Practical Optimization Strategy

Suppose you have a slow dashboard.

Don’t immediately write:

. useMemo();
. useCallback();
. React.memo();

Instead:

Step 1 — Measure

Use the React Profiler.

Dashboard
↓
ProductTable
↓
1000 rows

Step 2 — Identify the bottleneck

Maybe the actual problem is: 1000 DOM rows

Step 3 — Virtualize

1000 rows

↓

20 visible rows

Step 4 — Find expensive calculations

If sorting/filtering is expensive: useMemo(…)

Step 5 — Find unnecessary child renders

If a memoized child receives changing function references: useCallback(…)

Step 6 — Measure again

Confirm that the optimization actually improved performance.

Performance Optimization Checklist

Before shipping a React application, ask:

Rendering

  • Are unnecessary components re-rendering?
  • Is state located at the correct level?
  • Are expensive components isolated?
  • Are list keys stable?

Computation

  • Are expensive calculations repeated unnecessarily?
  • Would useMemo provide a meaningful benefit?
  • Are unnecessary effects causing additional renders?

Functions / References

  • Are memoized children receiving unstable callbacks?
  • Is useCallback actually needed?

Lists

  • Are large lists virtualized?
  • Are thousands of DOM nodes being rendered unnecessarily?

Network

  • Are API requests duplicated?
  • Is search debounced?
  • Can stale requests be cancelled?
  • Are responses cached appropriately?

Bundle

  • Is code split?
  • Are large features lazy-loaded?
  • Are unnecessary dependencies included?

Assets

  • Are images appropriately sized?
  • Are images optimized?
  • Are fonts and other assets loaded efficiently?

UX

  • Is LCP good?
  • Is INP good?
  • Is CLS controlled?
  • Are loading and error states handled properly?

Thanks for Reading! 👋

If you found this article helpful, consider following me on Medium for more in-depth articles on JavaScript, React, and modern web development.

I’d love to hear your thoughts! If you have any questions, suggestions, or a different perspective, leave a comment below. Your feedback helps me write better content and creates meaningful discussions within the developer community.

If this article added value to your learning, feel free to share it with other developers who might benefit from it.

Happy Coding! 🚀

— Salim Ansari

--

Salim Ansari
Salim Ansari

Written by Salim Ansari

Frontend Engineer with 3+ years of experience at Network18, building Firstpost & CNBCTV18.com. Sharing JavaScript, React & frontend insights.