React re-renders are not inherently bad — the framework is designed to handle them efficiently. But unnecessary re-renders of expensive components or large lists can degrade performance significantly. The key is knowing when optimization is needed and choosing the right tool.
Measure First, Optimize Second
Before reaching for useMemo or React.memo, profile your application with React DevTools Profiler. Identify which components are actually slow and how often they re-render. Optimizing components that render in 0.1ms wastes development time and adds code complexity with zero user benefit.
React.memo — For Expensive Component Trees
React.memo prevents a component from re-rendering if its props have not changed (shallow equality). Use it for components that are expensive to render and receive stable props:
const ExpensiveList = React.memo(function ExpensiveList({ items }) {
return items.map(item => <Item key={item.id} data={item} />);
});
Do not wrap every component with React.memo — the overhead of the equality check outweighs the benefit for cheap components.
useCallback — Stable Function References
Functions defined in render bodies are recreated on every render, causing React.memo children to always re-render. useCallback memoizes the function reference:
const handleClick = useCallback(() => {
doSomething(id);
}, [id]); // Only changes when id changes
useMemo — Expensive Computations
useMemo memoizes the result of an expensive calculation:
const sortedItems = useMemo(() => {
return [...items].sort((a, b) => a.price - b.price);
}, [items]);
Only use useMemo when the computation is genuinely expensive (sorting/filtering large arrays, complex math). Do not use it for simple property access or minor transformations.
Code Splitting with Dynamic Imports
Large JavaScript bundles increase Time to Interactive. Split your bundle by importing heavy components lazily:
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('./HeavyChart'), {
loading: () => <ChartSkeleton />,
ssr: false,
});
Virtualization for Long Lists
Rendering 1,000 list items in the DOM is always slow. Use virtualization to render only the visible items. React Virtual and TanStack Virtual are excellent options for Next.js applications.
State Colocation
State placed higher in the tree than necessary causes more components to re-render when it changes. Move state as close as possible to the components that use it. This is often more impactful than any memoization strategy.
Conclusion
React performance optimization is about measurement, not guesswork. Profile first, identify actual bottlenecks, then apply the appropriate tool. State colocation and code splitting often yield bigger wins than memoization. When you do memoize, confirm with the Profiler that it actually reduced renders.