React Performance Optimization Checklist

Results you can expect: Reduce bundle size by 40%, improve initial page load by 60%, eliminate memory leaks, and create lightning-fast user experiences.

1. Performance Measurement & Audit

✅ Set up React DevTools Profiler

Before optimizing, measure. Install React DevTools and enable the Profiler to identify actual bottlenecks.

// Add this to your development environment
if (process.env.NODE_ENV === 'development') {
  const whyDidYouRender = require('@welldone-software/why-did-you-render');
  whyDidYouRender(React, {
    trackAllPureComponents: true,
  });
}

✅ Track Core Web Vitals

Monitor LCP, FID, and CLS to understand real user performance impact.

// Custom hook for performance monitoring
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';

function useWebVitals() {
  useEffect(() => {
    getCLS(console.log);
    getFID(console.log);
    getFCP(console.log);
    getLCP(console.log);
    getTTFB(console.log);
  }, []);
}

✅ Analyze bundle size with Webpack Bundle Analyzer

Visualize what's making your bundle large.

npm install --save-dev webpack-bundle-analyzer
npx webpack-bundle-analyzer build/static/js/*.js

2. Bundle Size Optimization

✅ Implement route-based code splitting

Split your app by routes to load only necessary code.

import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

// Lazy load route components
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/profile" element={<Profile />} />
      </Routes>
    </Suspense>
  );
}

✅ Split large components

Break down heavy components into smaller, loadable chunks.

// Instead of importing heavy components directly
const DataVisualization = lazy(() => import('./DataVisualization'));
const ChartLibrary = lazy(() => import('./ChartLibrary'));

function Dashboard() {
  const [showCharts, setShowCharts] = useState(false);

  return (
    <div>
      <h1>Dashboard</h1>
      {showCharts && (
        <Suspense fallback={<div>Loading charts...</div>}>
          <DataVisualization />
          <ChartLibrary />
        </Suspense>
      )}
    </div>
  );
}

✅ Optimize imports for tree shaking

Import only what you need from large libraries.

// ❌ Bad - imports entire library
import * as _ from 'lodash';
import { Button, TextField, Dialog } from '@mui/material';

// ✅ Good - imports only needed functions
import debounce from 'lodash/debounce';
import groupBy from 'lodash/groupBy';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';

3. Component Rendering Optimization

✅ Use React.memo for pure components

Prevent unnecessary re-renders of components with stable props.

// ❌ Re-renders on every parent update
function ExpensiveComponent({ data, onUpdate }) {
  const processedData = expensiveCalculation(data);
  return <div>{processedData}</div>;
}

// ✅ Only re-renders when props change
const ExpensiveComponent = React.memo(({ data, onUpdate }) => {
  const processedData = expensiveCalculation(data);
  return <div>{processedData}</div>;
}, (prevProps, nextProps) => {
  // Custom comparison function
  return prevProps.data.id === nextProps.data.id;
});

✅ Optimize expensive calculations with useMemo

Cache calculation results between renders.

function ProductList({ products, filters }) {
  // ✅ Memoized calculation
  const filteredProducts = useMemo(() => {
    return products
      .filter(product => matchesFilters(product, filters))
      .sort((a, b) => a.price - b.price);
  }, [products, filters]);

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

✅ Stabilize function references with useCallback

Prevent child re-renders caused by new function references.

function TodoApp() {
  const [todos, setTodos] = useState([]);

  // ✅ Stable function reference
  const handleAddTodo = useCallback((text) => {
    setTodos(prev => [...prev, { id: Date.now(), text }]);
  }, []);

  const handleToggleTodo = useCallback((id) => {
    setTodos(prev => prev.map(todo => 
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  }, []);

  return (
    <div>
      <AddTodoForm onAdd={handleAddTodo} />
      <TodoList todos={todos} onToggle={handleToggleTodo} />
    </div>
  );
}

4. Advanced Optimization Patterns

✅ Implement virtual scrolling for large lists

Handle thousands of items efficiently.

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

function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      <ItemComponent item={items[index]} />
    </div>
  );

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

✅ Optimize Context usage

Split contexts and memoize values to prevent cascade re-renders.

// ✅ Split into focused contexts
const UserContext = createContext();
const ThemeContext = createContext();
const DataContext = createContext();

function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  
  // Memoize context value to prevent unnecessary re-renders
  const value = useMemo(() => ({
    user,
    setUser,
  }), [user]);

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

✅ Debounce expensive operations

Reduce frequency of expensive operations like API calls.

import { debounce } from 'lodash';

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

  // Debounced search function
  const debouncedSearch = useCallback(
    debounce(async (searchQuery) => {
      const results = await searchAPI(searchQuery);
      setResults(results);
    }, 300),
    []
  );

  useEffect(() => {
    if (query.length > 2) {
      debouncedSearch(query);
    }
  }, [query, debouncedSearch]);

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

5. State Management Performance

✅ Normalize complex state shapes

Avoid deeply nested updates that cause wide re-renders.

// ✅ Normalized state structure
const [users, setUsers] = useState({
  1: { id: 1, name: 'John', postIds: [1] }
});
const [posts, setPosts] = useState({
  1: { id: 1, title: 'Post 1', userId: 1 }
});

✅ Use reducer for complex state logic

Centralize state updates and optimize with immer.

import { useImmerReducer } from 'use-immer';

function todosReducer(draft, action) {
  switch (action.type) {
    case 'ADD_TODO':
      draft.push({ id: Date.now(), text: action.text, completed: false });
      break;
    case 'TOGGLE_TODO':
      const todo = draft.find(todo => todo.id === action.id);
      if (todo) todo.completed = !todo.completed;
      break;
    default:
      break;
  }
}

function TodoApp() {
  const [todos, dispatch] = useImmerReducer(todosReducer, []);
  // Component implementation...
}

6. Network & Data Fetching Optimization

✅ Implement request deduplication

Prevent duplicate API calls for the same data.

// Custom hook with request deduplication
const requestCache = new Map();

function useApiData(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    // Check if request is already in progress
    if (requestCache.has(url)) {
      requestCache.get(url).then(setData);
      return;
    }

    setLoading(true);
    const request = fetch(url).then(res => res.json());
    requestCache.set(url, request);

    request
      .then(data => {
        setData(data);
        setLoading(false);
      })
      .catch(() => {
        requestCache.delete(url);
        setLoading(false);
      });

    // Clean up cache after some time
    setTimeout(() => requestCache.delete(url), 30000);
  }, [url]);

  return { data, loading };
}

✅ Implement intelligent caching

Cache API responses with automatic invalidation.

import { QueryClient, QueryClientProvider, useQuery } from 'react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      cacheTime: 10 * 60 * 1000, // 10 minutes
      retry: 3,
      refetchOnWindowFocus: false,
    },
  },
});

function useProducts() {
  return useQuery('products', fetchProducts, {
    select: useCallback((data) => {
      // Transform data to reduce re-renders
      return data.map(product => ({
        id: product.id,
        name: product.name,
        price: product.price,
      }));
    }, []),
  });
}

Performance Monitoring & Alerts

✅ Set up automated performance monitoring

Track performance regressions in production.

// Performance monitoring utility
function trackPerformance(metricName, value) {
  // Send to analytics service
  if (typeof window !== 'undefined' && window.gtag) {
    window.gtag('event', 'timing_complete', {
      name: metricName,
      value: Math.round(value),
    });
  }
}

// Custom hook for performance tracking
function usePerformanceTracking(componentName) {
  useEffect(() => {
    const startTime = performance.now();
    
    return () => {
      const endTime = performance.now();
      trackPerformance(`${componentName}_render_time`, endTime - startTime);
    };
  }, [componentName]);
}

Common Performance Anti-patterns to Avoid

❌ Don't memoize everything

Memoization has overhead. Only memoize expensive calculations or frequently changing props.

// ❌ Unnecessary memoization
const simpleValue = useMemo(() => props.name, [props.name]);

// ✅ Direct usage is more efficient
const simpleValue = props.name;

❌ Don't create objects in render

Object creation in render causes unnecessary re-renders.

// ❌ New object on every render
function MyComponent({ items }) {
  return <List items={items} config={{ sortable: true, filterable: false }} />;
}

// ✅ Stable object reference
const LIST_CONFIG = { sortable: true, filterable: false };

function MyComponent({ items }) {
  return <List items={items} config={LIST_CONFIG} />;
}

Results You Can Expect

By implementing these optimization techniques, you can expect:

  • 40% reduction in bundle size through effective code splitting and tree shaking
  • 60% improvement in First Contentful Paint with optimized component architecture
  • Elimination of memory leaks that cause performance degradation
  • 70% reduction in API calls through intelligent caching strategies
  • Smooth 60fps animations even on low-end devices
  • Improved Core Web Vitals scores leading to better SEO rankings

Next Steps

  1. Audit your current app using React DevTools Profiler
  2. Start with the biggest impact items - usually bundle size and expensive renders
  3. Measure before and after each optimization
  4. Set up monitoring to catch performance regressions
  5. Make performance a team priority by establishing performance budgets

Remember: Premature optimization is the root of all evil, but strategic optimization based on real data can dramatically improve user experience.


This guide is based on real-world experience optimizing React applications serving millions of users. For personalized optimization help, contact me directly.