Performance Optimization in React

By Syncfusion®, Inc. · Last updated

Why performance matters, how React renders, avoiding extra re-renders, memoization with React.memo, useMemo and useCallback, lazy loading, code splitting and best practices.

Why Performance Matters

Performance means how fast your app feels. A fast app opens quickly, and reacts at once when the user clicks or types.

Speed matters because people leave slow apps. It matters most on cheap phones and slow networks, which many of your users may have.

There are two kinds of slowness:

  • Slow loading. The app downloads too much code before it can show anything.
  • Slow updates. The app does too much work each time something changes.

This lesson shows simple ways to fix both.

Real-World Example

A bookshop's search page shows 500 books. Each key press in the search box rendered all 500 cards again, so typing felt slow.

The team wraps BookCard in memo, sorts the list with useMemo, and passes handlers made with useCallback. Now a key press renders only the cards that changed.

Most React apps are fast enough without any tricks. First make your code correct and clear. Optimize only when something feels slow, and measure before and after.

React Rendering and Performance

To render means React calls your component function to get its TSX. Then React compares the new result with the old one, and changes only the parts of the real page that are different.

A component renders again when:

  • its own state changes,
  • its parent renders again, or
  • a context it reads changes.

The second point surprises many beginners. When a parent renders, all its children render too, even if their props did not change.

When the parent's state changes, React re-renders the parent and every child inside it

App state: count

Header

Counter

BookList

Only Counter shows the count, but all three children render again.

Each component below prints a line every time it renders. Click the button and watch the console. Header and BookList render again, even though nothing in them changed.

Avoiding Unnecessary Re-renders

A re-render is unnecessary when it gives the same result as before. A few renders do no harm. They become a problem when a component is big or slow, such as a list with thousands of rows.

Simple ways to avoid them, before any special tools:

  • Keep state low. Put state in the smallest component that needs it. Then only that component re-renders.
  • Do not copy data into state if you can work it out from props or other state.
  • Use stable keys in lists (like book.id), so React can reuse rows.

Keep fast-changing state low: move it into the small component that uses it State at the top State moved down

App (query)

SearchBox

BigList

App

SearchBox has query

BigList

Typing now re-renders only SearchBox, not the big list.

Below, the search text lives in SearchBox, not in App. Type in the box. Only SearchBox renders. BookList stays quiet.

Memoization Techniques

What is Memoization

Memoization means remembering a result so you do not work it out again. If the inputs are the same as last time, you reuse the old answer.

React gives you three tools for this:

  • React.memo remembers a component's output. It skips a render when the props are the same.
  • useMemo remembers a value, such as a filtered list.
  • useCallback remembers a function, so it stays the same between renders.

React.memo

Wrap a component in memo to skip renders when its props have not changed:

const BookList = memo(function BookList({ books }: Props) { ... });

Before each render, React compares the old props with the new props. If every prop is the same, React reuses the last result.

React.memo compares the old and new props. Same props: skip. Different props: render

new render

React.memo props the same?

yes

skip, reuse old

no

render again

Memo saves work only when the props really stay the same.

Click the button a few times. App renders each time, but Header renders only once, because its prop shop is always "Book Corner".

There is a trap. React compares props with ===. A new object or a new function is created on every render, so it is never === the old one. Then memo does nothing.

  • useMemo(() => value, [deps]) keeps the same object until a dependency changes.
  • useCallback(fn, [deps]) keeps the same function until a dependency changes.

In the lab below, onSelect is wrapped in useCallback. Remove useCallback and see BookList render on every click.

Do not wrap everything in memo, useMemo and useCallback. Each one has a small cost too. Use them for big or slow components, or when the console shows many wasted renders.

Loading and Bundle Optimization

When you build a React app, the build tool joins all your code into one or more JavaScript files called bundles. The browser must download a bundle before it can show the page. A big bundle means a slow start.

Lazy Loading Components

Lazy loading means loading a component only when it is needed, not at the start. React gives you lazy and Suspense for this:

const Reviews = lazy(() => import("./Reviews"));
  • import("./Reviews") (with brackets) loads the file later, when the code runs. The file must have a default export.
  • lazy(...) turns that into a component you can use like any other.
  • <Suspense fallback={...}> shows the fallback while the component is still loading.

The playground loads files at once, so slowImport.ts waits one second to act like a slow network. In a real app, you write lazy(() => import("./Reviews")) and the network gives you the wait.

Code Splitting

Code splitting means breaking the app's code into several smaller bundles. Each import() with brackets tells the build tool (like Vite): "put this file in its own bundle". So lazy is how you split code in React.

Code splitting: the first download is small, and other parts load only when needed One big bundle Split bundles

app.js everything: 900 KB

main.js 200 KB

reviews.js loads later

admin.js loads later

The user sees the page sooner, because the first file is smaller.

The most common place to split is by page. Each route gets its own bundle, and a user downloads a page only when they visit it.

const AdminPage = lazy(() => import("./pages/AdminPage"));

<Suspense fallback={<p>Loading...</p>}>
  <Routes>
    <Route path="/admin" element={<AdminPage />} />
  </Routes>
</Suspense>

Component Splitting Strategies

How you split components also affects speed. Some simple rules:

  • Split by job. A component that does one thing is easier to memo and to reuse.
  • Separate fast parts from slow parts. Put a quickly changing input in its own small component, away from a big list.
  • Pass children. A wrapper that takes children does not re-render them when its own state changes, because the children were made by the parent.
  • Lazy-load rare parts. Dialogs, charts and admin pages that few users open can load later.

The lab below uses the children pattern. ColorBox has its own state, but BigList, passed as children, does not re-render when the colour changes.

Performance Optimization Best Practices

  • Measure first. Use the Profiler tab in React Developer Tools to see which components render and how long they take.
  • Build for production to test speed. npm run dev adds extra checks and is slower than npm run build.
  • Keep state close to where it is used.
  • Give list items stable keys, like an id, not the array index.
  • Memoize only slow parts with memo, useMemo and useCallback.
  • Lazy-load pages and rare parts so the first bundle is small.
  • Show long lists in pages, or only the rows on screen, instead of thousands at once.
  • Optimize images: use the right size and a modern format.

Easy Way to Remember: measure, keep state low, memo the slow parts, lazy-load the rest.

Common mistakes

MistakeWhat you seeFix
Wrapping every component in memoMore code, no speed gainMemo only big or slow components
Passing a new object or arrow function to a memo componentIt still renders every timeUse useMemo or useCallback
Missing items in a useMemo dependency listOld values show on screenList every value used inside
Keeping input state at the top of the appTyping feels slow in big pagesMove the state into the input's component
Using lazy without SuspenseAn error when the component loadsWrap it in <Suspense fallback={...}>
Lazy-loading a file with only named exportsAn error: no default exportAdd export default to the component
Testing speed with npm run devThe app looks slower than it isTest a production build

Check Yourself

Exercises

What to Remember

  • A component renders when its state, its parent or its context changes. Keep state low to keep renders small.
  • memo skips renders when props are the same. useMemo and useCallback keep objects and functions the same.
  • lazy + Suspense load parts of the app later. This is code splitting, and it makes the first load faster. Measure before you optimize.

Next: Capstone - Task Management