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.memoremembers a component's output. It skips a render when the props are the same.useMemoremembers a value, such as a filtered list.useCallbackremembers 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 adefaultexport.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
childrendoes 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 devadds extra checks and is slower thannpm 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,useMemoanduseCallback. - 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
| Mistake | What you see | Fix |
|---|---|---|
Wrapping every component in memo | More code, no speed gain | Memo only big or slow components |
| Passing a new object or arrow function to a memo component | It still renders every time | Use useMemo or useCallback |
Missing items in a useMemo dependency list | Old values show on screen | List every value used inside |
| Keeping input state at the top of the app | Typing feels slow in big pages | Move the state into the input's component |
Using lazy without Suspense | An error when the component loads | Wrap it in <Suspense fallback={...}> |
| Lazy-loading a file with only named exports | An error: no default export | Add export default to the component |
Testing speed with npm run dev | The app looks slower than it is | Test 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.
memoskips renders when props are the same.useMemoanduseCallbackkeep objects and functions the same.lazy+Suspenseload parts of the app later. This is code splitting, and it makes the first load faster. Measure before you optimize.
Next: Capstone - Task Management