Featured Post

React Performance Optimization: A Complete Guide

Master React performance optimization techniques including memo, useMemo, useCallback, code splitting, and more to build blazing-fast applications.

2 min read
By Claude

React Performance Optimization: A Complete Guide

Performance is crucial for modern web applications. Users expect instant interactions and smooth experiences. In this comprehensive guide, I'll share proven techniques to optimize your React applications.

Understanding React Rendering

Before optimizing, understand how React renders components:

// React re-renders when:
// 1. State changes
// 2. Props change
// 3. Parent component re-renders
// 4. Context value changes

function ParentComponent() {
  const [count, setCount] = useState(0)

  // Child re-renders even if its props don't change
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Count: {count}
      </button>
      <ExpensiveChild />
    </div>
  )
}

1. React.memo for Component Memoization

Prevent unnecessary re-renders by memoizing components:

import { memo } from 'react'

interface UserCardProps {
  user: {
    id: string
    name: string
    email: string
  }
}

// Without memo - re-renders on every parent update
function UserCard({ user }: UserCardProps) {
  console.log('Rendering UserCard')
  return (
    <div>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  )
}

// With memo - only re-renders when user changes
export default memo(UserCard)

// Custom comparison function
export default memo(UserCard, (prevProps, nextProps) => {
  return prevProps.user.id === nextProps.user.id
})

2. useMemo for Expensive Calculations

Memoize expensive computations:

import { useMemo } from 'react'

function ProductList({ products }: { products: Product[] }) {
  // ❌ Runs on every render
  const sortedProducts = products.sort((a, b) => b.price - a.price)

  // ✅ Only recalculates when products change
  const sortedProducts = useMemo(() => {
    console.log('Sorting products...')
    return products.sort((a, b) => b.price - a.price)
  }, [products])

  return (
    <div>
      {sortedProducts.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  )
}

3. useCallback for Function References

Stabilize function references to prevent unnecessary re-renders:

import { useState, useCallback, memo } from 'react'

function TodoList() {
  const [todos, setTodos] = useState<Todo[]>([])

  // ❌ New function on every render
  const handleDelete = (id: string) => {
    setTodos(todos.filter(todo => todo.id !== id))
  }

  // ✅ Stable function reference
  const handleDelete = useCallback((id: string) => {
    setTodos(prev => prev.filter(todo => todo.id !== id))
  }, [])

  return (
    <div>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onDelete={handleDelete}
        />
      ))}
    </div>
  )
}

// TodoItem won't re-render if onDelete reference is stable
const TodoItem = memo(({ todo, onDelete }: TodoItemProps) => {
  return (
    <div>
      <span>{todo.title}</span>
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </div>
  )
})

4. Code Splitting with React.lazy

Split your bundle for faster initial loads:

import { lazy, Suspense } from 'react'
import { LoadingSpinner } from './LoadingSpinner'

// Lazy load heavy components
const Dashboard = lazy(() => import('./Dashboard'))
const Analytics = lazy(() => import('./Analytics'))
const Settings = lazy(() => import('./Settings'))

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/analytics" element={<Analytics />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  )
}

5. Virtualization for Long Lists

Use virtualization for lists with many items:

import { FixedSizeList } from 'react-window'

function VirtualizedList({ items }: { items: string[] }) {
  const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
    <div style={style}>
      Item {items[index]}
    </div>
  )

  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  )
}

6. Debouncing and Throttling

Control the rate of expensive operations:

import { useState, useMemo } from 'react'

function SearchInput() {
  const [query, setQuery] = useState('')

  // Debounce search - wait for user to stop typing
  const debouncedSearch = useMemo(() => {
    let timeout: NodeJS.Timeout
    return (value: string) => {
      clearTimeout(timeout)
      timeout = setTimeout(() => {
        performSearch(value)
      }, 300)
    }
  }, [])

  return (
    <input
      type="text"
      value={query}
      onChange={(e) => {
        setQuery(e.target.value)
        debouncedSearch(e.target.value)
      }}
    />
  )
}

// Custom hook for debouncing
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value)

  useEffect(() => {
    const timeout = setTimeout(() => {
      setDebouncedValue(value)
    }, delay)

    return () => clearTimeout(timeout)
  }, [value, delay])

  return debouncedValue
}

7. Optimize Context Usage

Prevent unnecessary context re-renders:

import { createContext, useContext, useMemo } from 'react'

// ❌ Single context with all state
const AppContext = createContext({
  user: null,
  theme: 'light',
  settings: {}
})

// ✅ Split contexts by update frequency
const UserContext = createContext(null)
const ThemeContext = createContext('light')
const SettingsContext = createContext({})

// Memoize context values
function UserProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState(null)

  // Prevent re-renders when user object reference changes
  const value = useMemo(() => ({ user, setUser }), [user])

  return (
    <UserContext.Provider value={value}>
      {children}
    </UserContext.Provider>
  )
}

8. Use Keys Correctly

Proper key usage helps React identify changes:

// ❌ Using index as key (anti-pattern for dynamic lists)
{items.map((item, index) => (
  <Item key={index} data={item} />
))}

// ✅ Using stable unique identifier
{items.map((item) => (
  <Item key={item.id} data={item} />
))}

// ✅ For static lists, index is fine
{['Home', 'About', 'Contact'].map((link, index) => (
  <NavLink key={index}>{link}</NavLink>
))}

9. Optimize Images

Images are often the biggest performance bottleneck:

import Image from 'next/image'

function ProductGallery() {
  return (
    <div>
      {/* ✅ Next.js Image with automatic optimization */}
      <Image
        src="/product.jpg"
        alt="Product"
        width={800}
        height={600}
        placeholder="blur"
        loading="lazy"
        quality={75}
      />

      {/* ✅ Native lazy loading */}
      <img
        src="/banner.jpg"
        alt="Banner"
        loading="lazy"
        decoding="async"
      />
    </div>
  )
}

10. Use Web Workers for Heavy Computations

Offload heavy computations to web workers:

// worker.ts
self.addEventListener('message', (e) => {
  const result = heavyComputation(e.data)
  self.postMessage(result)
})

// Component
function DataProcessor() {
  const [result, setResult] = useState(null)

  useEffect(() => {
    const worker = new Worker(new URL('./worker.ts', import.meta.url))

    worker.postMessage(largeDataset)
    worker.addEventListener('message', (e) => {
      setResult(e.data)
    })

    return () => worker.terminate()
  }, [])

  return <div>{result}</div>
}

11. Profile Before Optimizing

Use React DevTools Profiler:

import { Profiler } from 'react'

function App() {
  const onRenderCallback = (
    id: string,
    phase: 'mount' | 'update',
    actualDuration: number,
    baseDuration: number,
    startTime: number,
    commitTime: number
  ) => {
    console.log(`${id} took ${actualDuration}ms to render`)
  }

  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <Dashboard />
    </Profiler>
  )
}

12. Avoid Inline Functions and Objects

Inline definitions create new references on every render:

// ❌ Inline object - new reference each render
<UserProfile user={{ name: 'John', age: 30 }} />

// ✅ Stable reference
const user = { name: 'John', age: 30 }
<UserProfile user={user} />

// ❌ Inline function - new reference each render
<Button onClick={() => handleClick(id)} />

// ✅ Use useCallback
const handleButtonClick = useCallback(() => handleClick(id), [id])
<Button onClick={handleButtonClick} />

Performance Checklist

  • ✅ Use React.memo for expensive components
  • ✅ Memoize expensive calculations with useMemo
  • ✅ Stabilize function references with useCallback
  • ✅ Implement code splitting with lazy loading
  • ✅ Virtualize long lists
  • ✅ Debounce/throttle expensive operations
  • ✅ Optimize context usage
  • ✅ Use correct keys in lists
  • ✅ Lazy load images
  • ✅ Profile before optimizing
  • ✅ Avoid inline functions and objects in JSX

Conclusion

Performance optimization is about finding the right balance. Don't optimize prematurely - measure first, then optimize the bottlenecks. Use React DevTools Profiler to identify slow components and apply these techniques strategically.

Remember: Premature optimization is the root of all evil. Start with clean, readable code, then optimize based on real performance metrics.

Tools and Resources

Happy optimizing! ⚡

Published on December 22, 2024

Related Posts

Enjoyed this post?

Subscribe to get notified when I publish new content about web development and technology.