Skip to content
All essays
WebMarch 16, 202411 min

React Performance Patterns

Essential techniques and patterns for optimizing React application performance

Ü
Ümit Uz
Mobile & Full Stack Developer

Performance optimization is crucial for delivering excellent user experiences. React provides many tools and patterns for building fast applications, but knowing when and how to use them is key. Let's explore the most important performance patterns.

Understanding React's Rendering Model

Before optimizing, it's essential to understand how React renders. React re-renders components when state or props change. By default, all children re-render when their parent re-renders. This is usually fine, but can become a problem in large applications.

Virtual DOM Diffing

React uses a virtual DOM to minimize actual DOM manipulations. It compares the new virtual DOM with the previous one and only applies necessary changes to the real DOM. However, the diffing process itself has a cost, and avoiding unnecessary renders is still important.

Memoization Patterns

Memoization is the most common performance optimization technique in React. It involves caching the results of expensive computations and reusing them when inputs haven't changed.

React.memo for Component Memoization

React.memo is a higher-order component that memoizes the result. If props haven't changed, React skips re-rendering the component and reuses the last rendered result.

typescript
const ExpensiveComponent = React.memo(function ExpensiveComponent({ data }) { return <div>{/* Expensive rendering */}</div>; });

Use React.memo when: components render often with the same props, components are expensive to render, and components are pure given the same props.

Custom Comparison Functions

React.memo does a shallow comparison of props by default. For complex prop objects, you might need a custom comparison function.

typescript
const MyComponent = React.memo(function MyComponent({ user }) { return <div>{user.name}</div>; }, (prevProps, nextProps) => { return prevProps.user.id === nextProps.user.id; });

Be careful with custom comparison functions—poorly written comparisons can hurt performance more than helping.

useMemo for Expensive Calculations

useMemo caches the result of expensive calculations. The calculation only runs when dependencies change, not on every render.

typescript
const expensiveValue = useMemo(() => { return computeExpensiveValue(a, b); }, [a, b]);

When to Use useMemo

Use useMemo for: heavy computations that take noticeable time, calculations used in other Hooks' dependencies, and preventing object recreation for useCallback dependencies.

When Not to Use useMemo

Don't use useMemo for: simple calculations that are faster than memoization overhead, premature optimization without profiling, and every array or object creation.

useCallback for Function Stability

useCallback returns a memoized callback that only changes when dependencies change. This is useful when passing callbacks to optimized child components.

typescript
const handleClick = useCallback(() => { doSomething(dependency); }, [dependency]);

Preventing Child Re-renders

Child components wrapped in React.memo won't re-render if their props don't change. But functions are recreated on every render, so the child would re-render anyway. useCallback solves this.

typescript
function Parent() { const handleClick = useCallback(() => { console.log('clicked'); }, []); return <Child onClick={handleClick} />; } const Child = React.memo(function Child({ onClick }) { return <button onClick={onClick}>Click me</button>; });

List Rendering Optimization

Rendering large lists efficiently is a common performance challenge. React provides specific patterns for optimizing list rendering.

Key Props Matter

Always provide stable, unique keys for list items. This helps React identify which items have changed, been added, or been removed.

typescript
{items.map(item => ( <Item key={item.id} item={item} /> ))}

Virtual Scrolling

For very long lists (thousands of items), use virtual scrolling libraries like react-window or react-virtualized. These libraries only render visible items, dramatically improving performance.

typescript
import { FixedSizeList } from 'react-window'; function Row({ index, style }) { return <div style={style}>Row {index}</div>; } function MyList() { return ( <FixedSizeList height={400} itemCount={1000} itemSize={35} width={300} > {Row} </FixedSizeList> ); }

Code Splitting

Code splitting reduces the initial JavaScript bundle size by splitting code into smaller chunks that load on demand.

React.lazy for Component Code Splitting

React.lazy lets you dynamically import components. Combine it with Suspense to show loading state while the component loads.

typescript
const HeavyComponent = React.lazy(() => import('./HeavyComponent')); function App() { return ( <Suspense fallback={<Loading />}> <HeavyComponent /> </Suspense> ); }

Route-Based Splitting

Split code at the route level for the biggest impact. Each route loads only when needed.

typescript
const Home = React.lazy(() => import('./routes/Home')); const About = React.lazy(() => import('./routes/About'));

State Management Optimization

How you structure and update state affects performance significantly.

State Colocation

Keep state as close to where it's used as possible. Avoid lifting state higher than necessary. This prevents unnecessary re-renders of components that don't need the state.

Derived State

Don't store derived state in React state. Calculate it from existing state during render instead.

typescript
// ❌ Bad: Storing derived state const [items, setItems] = useState([]); const [itemCount, setItemCount] = useState(0); useEffect(() => { setItemCount(items.length); }, [items]); // ✅ Good: Calculate derived state const [items, setItems] = useState([]); const itemCount = items.length;

State Normalization

For complex state like lists of items, normalize your data structure. Store items in an object keyed by ID and store only IDs in arrays.

Context Optimization

Context can cause performance issues if not used carefully, as all consumers re-render when context value changes.

Split Contexts

Split contexts by how frequently values change. Separate static values from dynamic ones to prevent unnecessary re-renders.

typescript
const StaticContext = createContext(staticValue); const DynamicContext = createContext(dynamicValue);

Memoize Context Values

Memoize context values to prevent unnecessary consumer re-renders.

typescript
const value = useMemo(() => ({ user, logout }), [user]);

Image Optimization

Images are often the heaviest assets on a page. Optimize them to improve load times.

Lazy Loading Images

Lazy load images below the fold using native lazy loading or libraries.

typescript
<img src="image.jpg" loading="lazy" alt="Description" />

Modern Image Formats

Use modern image formats like WebP and AVIF for better compression. Serve fallback formats for browsers that don't support them.

Profiling and Measurement

Never optimize without measuring. React provides tools to identify performance bottlenecks.

React Profiler

Use React DevTools Profiler to identify slow components. It shows which components rendered and why.

Performance API

Use the browser's Performance API to measure specific operations.

typescript
const start = performance.now(); // ... operation const duration = performance.now() - start; console.log(`Operation took ${duration}ms`);

Common Performance Pitfalls

Avoid these common mistakes: memoizing everything without profiling, creating objects in render body without useMemo, inline functions in render without useCallback, updating state in useEffect without proper dependencies, and overusing Context for everything.

Conclusion

React performance optimization is about understanding the rendering model and applying the right patterns at the right time. Start by profiling to identify actual bottlenecks, then apply targeted optimizations. Remember that premature optimization is the root of all evil—measure first, optimize second.

Use memoization strategically, split code intelligently, and structure your state carefully. These patterns will help you build React applications that are fast, responsive, and provide excellent user experiences.

Next essay
GraphQL vs REST: Choosing the Right API Architecture