Performance optimization
Optimizing React apps isn’t about memoizing everything, it’s about knowing when and where to optimize. Most performance problems come from unnecessary re-renders, oversized bundles, or heavy computations inside the render cycle.
This section outlines practical strategies and ways to keep apps fast, responsive, and maintainable.
Core react optimizations
React.memo
- Use for functional components that render the same output for the same props.
- Avoid overusing by wrapping every component unnecessary use adds overhead.
- It’s only helpful for expensive or frequently re-rendered components (e.g., list items, charts).
useMemo
- Use to cache expensive calculations inside components.
- Avoid using it to wrap simple object literals or arrays like useMemo(() => [1,2,3], []).,This adds complexity without benefit.
- Best used for: filtering, sorting, or computations with high CPU cost.
useCallback
- Use only when passing functions to memoized children..
- Avoid “defensive wrapping” of every function, if there’s no re-render problem, you don’t need it.
Lazy loading and Dynamic Imports( Code-splitting )
- Use React.lazy() and Suspense to lazy load heavy components only when needed.
- Useful for modals, dashboards, sidebarsm charts or features not needed immediately.
const Chart = React.lazy(() => import('./Chart'))
- Dynamic imports: Most bundlers like Vite, Webpack support this out of the box, by split code into smaller chunks, when using dynamic imports.
- Bundle analysis tools (e.g., Webpack Bundle Analyzer) help identify and remove large dependencies.
Avoiding unnecessary re-renders
Prop drilling without memoization
If you pass down props through many layers, use memo, useMemo, or context only when needed. Prop changes ripple down; stabilize prop shapes where possible.
Stable function references
- Memoize callbacks( functions passed as props ) that cause children to re-render unnecessarily.
Avoid inline/anonymous functions in JSX (in hot paths)
In loops or re-render-heavy areas, extract handlers or memoize them.
// ❌Inefficient
<List onClick={() => doSomething(item)} />
// ✅ Better
const handleClick = useCallback(() => doSomething(item), [item])
<List onClick={handleClick} />
Memoize list items
- Wrap item components with
React.memoif lists are large or update frequently.
Limit use of context
- Avoid overusing global Context. It triggers re-renders for all consumers. For critical performance, prefer:
- Atomic contexts (multiple small contexts).
- External state libraries like Zustand.
Profiling and debugging tools
React DevTools Profiler
- Use the “Profiler” tab in React DevTools to inspect rendering performance
- Use to validate optimizations instead of guessing.
Why-did-you-render (development only)
- Use tools like why-did-you-rerender.
- Detects and logs unnecessary re-renders in memoized components.
- Add it to detect wasted renders in React.memo components.
import whyDidYouRender from '@welldone-software/why-did-you-render'
if (process.env.NODE_ENV === 'development') {
whyDidYouRender(React)
}
Lighthouse / Web Vitals
- Measure real-world performance metrics: load time, interaction delay, cumulative layout shift (CLS).
- Don’t guess the measure first. Only optimize what’s proven slow using real data (profilers, logs, user reports). Premature optimization creates complexity without benefit.
- Helps track performance regressions over time.
Code-splitting: Lazy loading routes and components (React.lazy, Suspense) to keep bundle size down in case of complex components.
Dependency boundaries
Ensure packages depend only on what’s necessary to avoid circular dependencies. Tools like npm-prune can help.







