Skip to main content
React Performance Optimization: Stop Re-rendering Everything

React Performance Optimization: Stop Re-rendering Everything

Web DevelopmentDecember 3, 202514 min read0 views
ReactJavaScriptPerformanceOptimizationFrontendWeb Development
Share:

React Performance Optimization: Stop Re-rendering Everything

React is fast, but it can get slow if you're not careful. Let's learn how to keep your React apps lightning-fast with proper optimization techniques.

The Performance Problem

React re-renders components when:

  • State changes
  • Props change
  • Parent component re-renders

The issue? Unnecessary re-renders waste CPU cycles and slow down your app.

React.memo: Prevent Unnecessary Re-renders

Without React.memo

// This re-renders every time parent re-renders function UserCard({ name, email }) { console.log("UserCard rendered"); return ( <div> <h3>{name}</h3> <p>{email}</p> </div> ); }

With React.memo

// Only re-renders when name or email changes const UserCard = React.memo(({ name, email }) => { console.log("UserCard rendered"); return ( <div> <h3>{name}</h3> <p>{email}</p> </div> ); });

Custom Comparison Function

const UserCard = React.memo( ({ user }) => { return ( <div> <h3>{user.name}</h3> <p>{user.email}</p> </div> ); }, (prevProps, nextProps) => { // Return true if props are equal (skip re-render) return prevProps.user.id === nextProps.user.id; } );

useMemo: Memoize Expensive Calculations

Without useMemo

function ProductList({ products }) { // Recalculates on every render! const sortedProducts = products .sort((a, b) => b.price - a.price) .filter(p => p.inStock); return ( <div> {sortedProducts.map(p => <ProductCard key={p.id} product={p} />)} </div> ); }

With useMemo

function ProductList({ products }) { // Only recalculates when products change const sortedProducts = useMemo(() => { console.log("Calculating sorted products"); return products .sort((a, b) => b.price - a.price) .filter(p => p.inStock); }, [products]); return ( <div> {sortedProducts.map(p => <ProductCard key={p.id} product={p} />)} </div> ); }

useCallback: Memoize Functions

The Problem

function Parent() { const [count, setCount] = useState(0); // New function created on every render const handleClick = () => { console.log("Clicked!"); }; return ( <> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <Child onClick={handleClick} /> {/* Child re-renders unnecessarily */} </> ); } const Child = React.memo(({ onClick }) => { console.log("Child rendered"); return <button onClick={onClick}>Click Me</button>; });

The Solution

function Parent() { const [count, setCount] = useState(0); // Same function reference across renders const handleClick = useCallback(() => { console.log("Clicked!"); }, []); // Dependencies array return ( <> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <Child onClick={handleClick} /> {/* Child doesn't re-render */} </> ); }

Code Splitting: Load What You Need

Route-Based Splitting

import { lazy, Suspense } from 'react'; // Instead of: import Dashboard from './Dashboard'; const Dashboard = lazy(() => import('./Dashboard')); const Profile = lazy(() => import('./Profile')); const Settings = lazy(() => import('./Settings')); function App() { return ( <Suspense fallback={<div>Loading...</div>}> <Routes> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/profile" element={<Profile />} /> <Route path="/settings" element={<Settings />} /> </Routes> </Suspense> ); }

Component-Based Splitting

const HeavyChart = lazy(() => import('./HeavyChart')); function Dashboard() { const [showChart, setShowChart] = useState(false); return ( <div> <button onClick={() => setShowChart(true)}>Show Chart</button> {showChart && ( <Suspense fallback={<div>Loading chart...</div>}> <HeavyChart /> </Suspense> )} </div> ); }

Virtualization: Render What's Visible

Without Virtualization (Slow)

function UserList({ users }) { // Renders 10,000 DOM elements! return ( <div> {users.map(user => ( <UserCard key={user.id} user={user} /> ))} </div> ); }

With Virtualization (Fast)

import { FixedSizeList } from 'react-window'; function UserList({ users }) { // Only renders visible rows (~20 DOM elements) return ( <FixedSizeList height={600} itemCount={users.length} itemSize={80} width="100%" > {({ index, style }) => ( <div style={style}> <UserCard user={users[index]} /> </div> )} </FixedSizeList> ); }

Debouncing and Throttling

Debounce: Wait for User to Stop Typing

import { useState, useCallback } from 'react'; import debounce from 'lodash/debounce'; function SearchInput() { const [query, setQuery] = useState(''); // Only search 500ms after user stops typing const debouncedSearch = useCallback( debounce((value) => { console.log('Searching for:', value); // API call here }, 500), [] ); const handleChange = (e) => { setQuery(e.target.value); debouncedSearch(e.target.value); }; return <input value={query} onChange={handleChange} />; }

Throttle: Limit Function Calls

import { useCallback } from 'react'; import throttle from 'lodash/throttle'; function ScrollTracker() { // Only execute once every 200ms const handleScroll = useCallback( throttle(() => { console.log('Scroll position:', window.scrollY); }, 200), [] ); useEffect(() => { window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); }, [handleScroll]); return <div>Scroll me!</div>; }

Context API Optimization

The Problem

// Every component re-renders when any context value changes const AppContext = createContext(); function App() { const [user, setUser] = useState(null); const [theme, setTheme] = useState('light'); return ( <AppContext.Provider value={{ user, setUser, theme, setTheme }}> <Header /> <Main /> <Footer /> </AppContext.Provider> ); }

The Solution: Split Contexts

const UserContext = createContext(); const ThemeContext = createContext(); function App() { const [user, setUser] = useState(null); const [theme, setTheme] = useState('light'); return ( <UserContext.Provider value={{ user, setUser }}> <ThemeContext.Provider value={{ theme, setTheme }}> <Header /> <Main /> <Footer /> </ThemeContext.Provider> </UserContext.Provider> ); }

Image Optimization

// Lazy load images import { LazyLoadImage } from 'react-lazy-load-image-component'; function ProductImage({ src, alt }) { return ( <LazyLoadImage src={src} alt={alt} effect="blur" threshold={100} width={300} height={300} /> ); } // Use next/image for automatic optimization import Image from 'next/image'; function ProductImage({ src, alt }) { return ( <Image src={src} alt={alt} width={300} height={300} loading="lazy" placeholder="blur" /> ); }

Performance Checklist

  • ✅ Use React.memo for expensive components
  • ✅ Use useMemo for expensive calculations
  • ✅ Use useCallback for function props
  • ✅ Implement code splitting for routes
  • ✅ Use virtualization for long lists
  • ✅ Debounce/throttle event handlers
  • ✅ Split Context into smaller contexts
  • ✅ Lazy load images
  • ✅ Use production build
  • ✅ Profile with React DevTools

When NOT to Optimize

❌ Don't optimize prematurely ❌ Don't use useMemo for simple calculations ❌ Don't use React.memo for all components ❌ Don't over-split contexts

Rule of thumb: Optimize when you measure a performance problem, not before.

Measuring Performance

import { Profiler } from 'react'; function onRenderCallback( id, phase, actualDuration, baseDuration, startTime, commitTime ) { console.log(`${id} took ${actualDuration}ms`); } function App() { return ( <Profiler id="App" onRender={onRenderCallback}> <Dashboard /> </Profiler> ); }

Conclusion

React performance optimization is about finding the right balance:

  • Profile first, optimize later
  • Use built-in tools (memo, useMemo, useCallback)
  • Split code intelligently
  • Virtualize long lists
  • Measure the impact

Remember: Fast apps = Happy users! 🚀⚡

Let's Connect

Ready to build something amazing together?

Send us a message

🚀

Let's Chat.

Tell me about your project.

Let's create something together 🤝

Visit my social profile and get connected