React is fast by default, but real-world applications need optimization. I've optimized React applications that were slow, and I've seen applications that were fast become slow as they grew. The difference? Understanding React's rendering model and applying optimization techniques strategically.
Understanding React's Rendering Model
React re-renders components when state or props change. This is powerful, but it can cause performance problems if not managed correctly.
I've debugged applications where a single state change caused hundreds of components to re-render unnecessarily. React DevTools Profiler revealed the problem, but fixing it required understanding why components were re-rendering.
The key insight: React re-renders components when their props or state change, even if the rendered output is identical. Preventing unnecessary re-renders is the foundation of React performance optimization.
React.memo: Preventing Unnecessary Re-renders
React.memo is a higher-order component that memoizes the result of a component. If props haven't changed, React skips re-rendering.
I use React.memo for components that:
- Render frequently but props change rarely
- Are expensive to render
- Are rendered in lists with many items
For a dashboard with hundreds of data points, wrapping list items in React.memo reduced re-renders by 80%. The parent component could update frequently, but list items only re-rendered when their specific data changed.
Important: React.memo does a shallow comparison by default. If props are objects or arrays, you might need a custom comparison function.
useMemo: Expensive Calculations
useMemo memoizes the result of expensive calculations. I use it when:
- Calculations are computationally expensive
- Calculations depend on props or state that change infrequently
- Results are used in multiple places
I've optimized data processing components where filtering and sorting large arrays happened on every render. Using useMemo to memoize the results, recalculating only when source data changed, improved performance significantly.
Don't overuse useMemo. Memoization has overhead—only use it when the calculation is actually expensive or when you've measured a performance problem.
useCallback: Stable Function References
useCallback returns a memoized callback function. I use it to prevent child components from re-rendering when parent components re-render.
If you pass a function as a prop to a memoized component, and that function is recreated on every render, the child component will re-render even if wrapped in React.memo. useCallback provides a stable function reference.
I've fixed performance issues where callback functions were causing unnecessary re-renders. useCallback solved the problem by ensuring function references remained stable.
Code Splitting: Reducing Initial Bundle Size
Large JavaScript bundles slow initial page load. Code splitting loads code only when needed.
React.lazy and Suspense make code splitting straightforward:
- Use React.lazy for route-based code splitting
- Use dynamic imports for component-based code splitting
- Use Suspense to handle loading states
For a large application, code splitting reduced initial bundle size from 2.5MB to 400KB. The rest loaded as users navigated to different sections.
I also use code splitting for heavy libraries. Loading chart libraries or rich text editors only when needed reduces initial load time.
Virtual DOM Optimization
React's virtual DOM is fast, but you can help it:
- Use keys correctly: Keys help React identify which items changed. Stable, unique keys improve diffing performance.
- Avoid inline object creation: Creating objects in render methods creates new references, causing unnecessary re-renders.
- Keep component trees shallow: Deep component trees increase diffing time.
I've refactored components that created objects in render methods. Moving object creation outside render or using useMemo improved performance.
Bundle Size Optimization
Smaller bundles load faster. I optimize bundle size by:
- Removing unused code (tree shaking)
- Using production builds (minification, dead code elimination)
- Analyzing bundle size (webpack-bundle-analyzer)
- Replacing heavy libraries with lighter alternatives
I've replaced entire libraries with lighter alternatives. Moment.js is 70KB—date-fns is modular and much smaller. Replacing Moment.js with date-fns reduced bundle size significantly.
Performance Monitoring
You can't optimize what you don't measure. I use:
- React DevTools Profiler to identify slow components
- Chrome DevTools Performance tab for overall performance
- Lighthouse for performance audits
- Real User Monitoring (RUM) for production performance
I profile applications before and after optimizations to measure impact. Sometimes optimizations don't help as much as expected, and sometimes small changes have big impacts.
Common Performance Pitfalls
I've seen these mistakes repeatedly:
Unnecessary Re-renders
Components re-rendering when they don't need to. React.memo, useMemo, and useCallback solve this.
Large Bundle Sizes
Including entire libraries when only small parts are needed. Tree shaking and code splitting help.
Inefficient List Rendering
Rendering thousands of items without virtualization. Libraries like react-window solve this.
Blocking the Main Thread
Expensive calculations blocking rendering. Web Workers or breaking work into chunks helps.
What I've Learned
After optimizing React applications:
- Measure first. Don't optimize without data showing a problem.
- Start with the biggest wins. Code splitting and bundle optimization often provide the most improvement.
- Use React DevTools. The Profiler reveals performance problems quickly.
- Don't over-optimize. Some re-renders are fine if they're fast.
- Test optimizations. Sometimes optimizations don't help or make things worse.
React performance optimization is about understanding React's rendering model and applying techniques strategically. Measure, optimize, measure again. Repeat until performance meets requirements.