DEV Community

SOVANNARO
SOVANNARO

Posted on

7 React Performance Patterns Every Developer Should Steal (and How to Implement Them)

In the world of React development, performance optimization isn't just a "nice-to-have" – it's a critical skill that separates good apps from great ones. After optimizing dozens of production React applications, I've distilled these 7 battle-tested performance patterns you can immediately implement in your projects.


1. Memoization Magic: useMemo & useCallback

The Problem: Unnecessary re-renders from unchanged props/state

The Solution: Cache expensive computations and function references

const ExpensiveComponent = ({ items }) => {
  // Only recalculates when `items` changes
  const sortedList = useMemo(() => 
    items.sort((a, b) => a.price - b.price), 
  [items]);

  // Stable function reference
  const handleClick = useCallback(() => {
    console.log('Item clicked:', sortedList[0]);
  }, [sortedList]);

  return <ChildComponent onClick={handleClick} />;
};
Enter fullscreen mode Exit fullscreen mode

When to Use:

  • Heavy computations (sorting, filtering, data transformations)
  • Passing callbacks to optimized child components (React.memo)
  • Context providers passing stable values

Pro Tip: Combine with React.memo for child components to prevent unnecessary tree updates.


2. Lazy Loading & Code Splitting

The Problem: Bloated initial bundle sizes slowing down FCP (First Contentful Paint)

The Solution: Dynamic imports + Suspense for gradual loading

const HeavyChartLibrary = React.lazy(() => import('./ChartComponent'));

const Dashboard = () => (
  <React.Suspense fallback={<Spinner />}>
    {showCharts && <HeavyChartLibrary />}
  </React.Suspense>
);
Enter fullscreen mode Exit fullscreen mode

Advanced Pattern: Route-based splitting with React Router:

const routes = [
  {
    path: '/analytics',
    element: (
      <React.Suspense fallback={null}>
        <AnalyticsPage />
      </React.Suspense>
    ),
  },
];
Enter fullscreen mode Exit fullscreen mode

When to Use:

  • Non-critical above-the-fold components
  • Routes
  • Heavy third-party libraries

3. Virtualized Lists for Massive Datasets

The Problem: DOM node overload from rendering thousands of items

The Solution: Render only visible items with react-window

import { FixedSizeList as List } from 'react-window';

const BigList = ({ items }) => (
  <List
    height={600}
    itemCount={items.length}
    itemSize={35}
    width="100%"
  >
    {({ index, style }) => (
      <div style={style}>{items[index].name}</div>
    )}
  </List>
);
Enter fullscreen mode Exit fullscreen mode

Bonus: For dynamic heights, use VariableSizeList. Pair with react-virtualized-auto-sizer for responsive containers.


4. Smart State Batching

The Problem: Multiple state updates triggering sequential re-renders

The Solution: Leverage React 18+ automatic batching

Classic Approach (Pre-React 18):

// Triggers two re-renders
setCount(1);
setText('Updated');
Enter fullscreen mode Exit fullscreen mode

Optimized with React 18 Automatic Batching:

// Single re-render
ReactDOM.flushSync(() => {
  setCount(1);
  setText('Updated');
});
Enter fullscreen mode Exit fullscreen mode

Advanced Pattern: Use useReducer for complex state updates:

const [state, dispatch] = useReducer(reducer, initialState);

// Single update
dispatch({ type: 'UPDATE_BOTH', count: 1, text: 'Updated' });
Enter fullscreen mode Exit fullscreen mode

5. Debounced API Calls in Hooks

The Problem: API spamming from rapid user input (search bars, filters)

The Solution: useDebounce custom hook

import { useEffect, useState } from 'react';

const useDebouncedValue = (value, delay) => {
  const [debouncedValue, setDebouncedValue] = useState(value);

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

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

  return debouncedValue;
};

// Usage in component
const SearchComponent = () => {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebouncedValue(query, 300);

  useEffect(() => {
    fetchResults(debouncedQuery); // API call
  }, [debouncedQuery]);
};
Enter fullscreen mode Exit fullscreen mode

Pro Tip: Combine with AbortController to cancel pending requests.


6. Optimized Context API Usage

The Problem: Context consumers re-rendering when unrelated values change

The Solution: Split contexts + memoization

Bad Practice:

// All consumers re-render when any value changes
const AppContext = createContext({ 
  user: null, 
  theme: 'light' 
});
Enter fullscreen mode Exit fullscreen mode

Optimized Approach:

// Split into logical contexts
const UserContext = createContext(null);
const ThemeContext = createContext('light');

// Memoized provider value
const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light');
  const value = useMemo(() => ({ theme, setTheme }), [theme]);

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

Advanced Pattern: Use context selectors with use-context-selector library.


7. Optimistic UI Updates

The Problem: Waiting for API responses creates sluggish interfaces

The Solution: Instant visual feedback + rollback on error

const TodoList = () => {
  const [todos, setTodos] = useState([]);

  const addTodo = async (text) => {
    const tempId = Date.now();

    // Optimistic update
    setTodos(prev => [...prev, { id: tempId, text, status: 'pending' }]);

    try {
      const savedTodo = await api.saveTodo(text);
      setTodos(prev => 
        prev.map(t => t.id === tempId ? savedTodo : t)
      );
    } catch (error) {
      // Rollback on error
      setTodos(prev => prev.filter(t => t.id !== tempId));
    }
  };
};
Enter fullscreen mode Exit fullscreen mode

Key Benefits:

  • Instant perceived performance
  • Reduced "loading spinner fatigue"
  • Smooth user experience

Putting It All Together: Performance Checklist

  1. Audit re-renders with React DevTools Profiler
  2. Measure bundle size with source-map-explorer
  3. Test with Chrome's Performance tab (CPU throttling)
  4. Use React.memo/useMemo/useCallback judiciously
  5. Implement incremental loading strategies
  6. Optimize images/media with lazy loading
  7. Server-side render critical content

Remember: Premature optimization is the root of all evil – profile first, optimize second!


FAQ: React Performance Patterns

Q: How do I choose between useMemo and useCallback?

A: useMemo for values, useCallback for functions

Q: Does React 18 automatic batching work everywhere?

A: Yes, except in async operations (wrap in flushSync if needed)

Q: When should I avoid virtualization?

A: For lists under 100 items – the overhead outweighs benefits


By implementing these patterns, you'll create React apps that feel snappier, consume less memory, and keep users engaged. The best part? All these techniques work with any React stack – whether you're using Next.js, Gatsby, or vanilla React.

Top comments (0)