Skip to content

React Performance Optimization - 2025 Guide

React performance optimization requires understanding when and how components re-render, effective use of memoization, and proper code splitting strategies.

1. Component Memoization

React.memo Usage

// Memoize expensive components
const ExpensiveComponent = React.memo(function ExpensiveComponent({ 
  data, 
  onAction 
}: {
  data: DataType[];
  onAction: (id: string) => void;
}) {
  const processedData = useMemo(() => {
    return data.map(item => ({
      ...item,
      computed: expensiveComputation(item)
    }));
  }, [data]);

  return (
    <div>
      {processedData.map(item => (
        <ExpensiveItem 
          key={item.id} 
          item={item} 
          onAction={onAction}
        />
      ))}
    </div>
  );
});

// Memoize item components
const ExpensiveItem = React.memo(function ExpensiveItem({
  item,
  onAction
}: {
  item: ProcessedDataType;
  onAction: (id: string) => void;
}) {
  const handleClick = useCallback(() => {
    onAction(item.id);
  }, [item.id, onAction]);

  return (
    <div onClick={handleClick}>
      {item.name}: {item.computed}
    </div>
  );
});

useMemo and useCallback

function DataVisualization({ data, filters }: Props) {
  // Memoize expensive calculations
  const filteredData = useMemo(() => {
    return data.filter(item => 
      filters.every(filter => filter.predicate(item))
    );
  }, [data, filters]);

  const chartData = useMemo(() => {
    return processChartData(filteredData);
  }, [filteredData]);

  // Memoize event handlers
  const handleFilterChange = useCallback((newFilters: Filter[]) => {
    setFilters(newFilters);
  }, []);

  const handleDataExport = useCallback(async () => {
    const exportData = await processExportData(filteredData);
    downloadFile(exportData);
  }, [filteredData]);

  return (
    <div>
      <FilterPanel onFilterChange={handleFilterChange} />
      <Chart data={chartData} />
      <ExportButton onExport={handleDataExport} />
    </div>
  );
}

2. Code Splitting and Lazy Loading

Component Lazy Loading

// Lazy load heavy components
const LazyDataTable = lazy(() => import('./DataTable'));
const LazyChart = lazy(() => import('./Chart'));
const LazyModal = lazy(() => import('./Modal'));

function Dashboard() {
  const [activeTab, setActiveTab] = useState('overview');
  const [showModal, setShowModal] = useState(false);

  return (
    <div>
      <TabNavigation activeTab={activeTab} onTabChange={setActiveTab} />

      <Suspense fallback={<TableSkeleton />}>
        {activeTab === 'data' && <LazyDataTable />}
      </Suspense>

      <Suspense fallback={<ChartSkeleton />}>
        {activeTab === 'analytics' && <LazyChart />}
      </Suspense>

      {showModal && (
        <Suspense fallback={<ModalSkeleton />}>
          <LazyModal onClose={() => setShowModal(false)} />
        </Suspense>
      )}
    </div>
  );
}

Route-Based Code Splitting

// Router with lazy loading
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

const HomePage = lazy(() => import('./pages/HomePage'));
const DashboardPage = lazy(() => import('./pages/DashboardPage'));
const AnalyticsPage = lazy(() => import('./pages/AnalyticsPage'));
const SettingsPage = lazy(() => import('./pages/SettingsPage'));

function App() {
  return (
    <Routes>
      <Route 
        path="/" 
        element={
          <Suspense fallback={<PageSkeleton />}>
            <HomePage />
          </Suspense>
        } 
      />
      <Route 
        path="/dashboard" 
        element={
          <Suspense fallback={<PageSkeleton />}>
            <DashboardPage />
          </Suspense>
        } 
      />
      <Route 
        path="/analytics" 
        element={
          <Suspense fallback={<PageSkeleton />}>
            <AnalyticsPage />
          </Suspense>
        } 
      />
    </Routes>
  );
}

3. Virtual Scrolling for Large Lists

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

interface VirtualizedListProps {
  items: ListItem[];
  onItemClick: (item: ListItem) => void;
}

function VirtualizedList({ items, onItemClick }: VirtualizedListProps) {
  const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => {
    const item = items[index];

    return (
      <div 
        style={style}
        onClick={() => onItemClick(item)}
        className="flex items-center p-4 border-b hover:bg-gray-50"
      >
        <img src={item.avatar} alt="" className="w-10 h-10 rounded-full mr-3" />
        <div>
          <div className="font-medium">{item.name}</div>
          <div className="text-sm text-gray-500">{item.email}</div>
        </div>
      </div>
    );
  };

  return (
    <List
      height={400}
      itemCount={items.length}
      itemSize={80}
      overscanCount={5}
    >
      {Row}
    </List>
  );
}

4. Image Optimization

// Lazy loading images with intersection observer
function LazyImage({ 
  src, 
  alt, 
  className,
  placeholder = '/placeholder.jpg'
}: {
  src: string;
  alt: string;
  className?: string;
  placeholder?: string;
}) {
  const [imageSrc, setImageSrc] = useState(placeholder);
  const [imageRef, setImageRef] = useState<HTMLImageElement | null>(null);

  useEffect(() => {
    let observer: IntersectionObserver;

    if (imageRef && imageSrc === placeholder) {
      observer = new IntersectionObserver(
        entries => {
          entries.forEach(entry => {
            if (entry.isIntersecting) {
              setImageSrc(src);
              observer.unobserve(imageRef);
            }
          });
        },
        { threshold: 0.1 }
      );
      observer.observe(imageRef);
    }

    return () => {
      if (observer && imageRef) {
        observer.unobserve(imageRef);
      }
    };
  }, [imageRef, imageSrc, placeholder, src]);

  return (
    <img
      ref={setImageRef}
      src={imageSrc}
      alt={alt}
      className={className}
      loading="lazy"
    />
  );
}

// Progressive image loading
function ProgressiveImage({ src, placeholder, alt, className }: {
  src: string;
  placeholder: string;
  alt: string;
  className?: string;
}) {
  const [imageSrc, setImageSrc] = useState(placeholder);
  const [loaded, setLoaded] = useState(false);

  useEffect(() => {
    const img = new Image();
    img.src = src;
    img.onload = () => {
      setImageSrc(src);
      setLoaded(true);
    };
  }, [src]);

  return (
    <div className={`relative overflow-hidden ${className}`}>
      <img
        src={imageSrc}
        alt={alt}
        className={`transition-opacity duration-300 ${
          loaded ? 'opacity-100' : 'opacity-70'
        }`}
      />
      {!loaded && (
        <div className="absolute inset-0 bg-gray-200 animate-pulse" />
      )}
    </div>
  );
}

5. State Management Optimization

// Split context to prevent unnecessary re-renders
const ThemeContext = createContext();
const UserContext = createContext();
const DataContext = createContext();

// Use selectors with Zustand
import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';

const useAppStore = create(
  subscribeWithSelector((set, get) => ({
    user: null,
    theme: 'light',
    data: [],
    filters: [],

    // Actions
    setUser: (user) => set({ user }),
    setTheme: (theme) => set({ theme }),
    setData: (data) => set({ data }),
    setFilters: (filters) => set({ filters }),
  }))
);

// Optimized selectors
function UserProfile() {
  // Only re-renders when user changes
  const user = useAppStore((state) => state.user);

  return <div>{user?.name}</div>;
}

function ThemeToggle() {
  // Only re-renders when theme changes
  const theme = useAppStore((state) => state.theme);
  const setTheme = useAppStore((state) => state.setTheme);

  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      {theme}
    </button>
  );
}

6. Performance Monitoring

// Performance monitoring hook
function usePerformanceMonitoring() {
  useEffect(() => {
    // Monitor Core Web Vitals
    if ('web-vital' in window) {
      import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
        getCLS(console.log);
        getFID(console.log);
        getFCP(console.log);
        getLCP(console.log);
        getTTFB(console.log);
      });
    }

    // Monitor long tasks
    if ('PerformanceObserver' in window) {
      const observer = new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          if (entry.duration > 50) {
            console.warn('Long task detected:', entry);
          }
        }
      });
      observer.observe({ entryTypes: ['longtask'] });

      return () => observer.disconnect();
    }
  }, []);
}

// React Profiler wrapper
function ProfiledComponent({ children, id }: {
  children: React.ReactNode;
  id: string;
}) {
  const onRenderCallback = (
    id: string,
    phase: "mount" | "update",
    actualDuration: number,
    baseDuration: number,
    startTime: number,
    commitTime: number
  ) => {
    if (actualDuration > 16) { // More than one frame
      console.warn(`Slow render in ${id}:`, {
        phase,
        actualDuration,
        baseDuration
      });
    }
  };

  return (
    <Profiler id={id} onRender={onRenderCallback}>
      {children}
    </Profiler>
  );
}

7. Bundle Optimization

// Vite configuration for optimization
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    react(),
    visualizer({
      filename: 'dist/stats.html',
      open: true,
      gzipSize: true,
    }),
  ],
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          router: ['react-router-dom'],
          ui: ['@mui/material', '@emotion/react'],
          charts: ['recharts', 'd3'],
        },
      },
    },
    chunkSizeWarningLimit: 500,
  },
  optimizeDeps: {
    include: ['react', 'react-dom', 'react-router-dom'],
  },
});

8. Memory Leak Prevention

// Cleanup subscriptions and timers
function useTimer() {
  const [time, setTime] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setTime(prev => prev + 1);
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  return time;
}

// Cleanup event listeners
function useWindowSize() {
  const [size, setSize] = useState({ width: 0, height: 0 });

  useEffect(() => {
    const updateSize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };

    window.addEventListener('resize', updateSize);
    updateSize();

    return () => window.removeEventListener('resize', updateSize);
  }, []);

  return size;
}

// Cleanup async operations
function useAsyncOperation() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let cancelled = false;

    const fetchData = async () => {
      setLoading(true);
      try {
        const result = await api.getData();
        if (!cancelled) {
          setData(result);
        }
      } catch (error) {
        if (!cancelled) {
          console.error(error);
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    };

    fetchData();

    return () => {
      cancelled = true;
    };
  }, []);

  return { data, loading };
}

Performance Checklist

Rendering Optimization

  • [ ] Use React.memo for expensive components
  • [ ] Implement useMemo for expensive calculations
  • [ ] Use useCallback for event handlers passed to children
  • [ ] Avoid creating objects/functions in render
  • [ ] Use key props correctly for lists

Code Splitting

  • [ ] Implement route-based code splitting
  • [ ] Lazy load heavy components
  • [ ] Split vendor bundles appropriately
  • [ ] Use dynamic imports for conditional features

State Management

  • [ ] Split context providers by concern
  • [ ] Use state selectors to prevent unnecessary re-renders
  • [ ] Avoid storing derived state
  • [ ] Implement proper state normalization

Assets and Images

  • [ ] Implement lazy loading for images
  • [ ] Use appropriate image formats (WebP, AVIF)
  • [ ] Implement image compression
  • [ ] Use CDN for static assets

Monitoring

  • [ ] Monitor Core Web Vitals
  • [ ] Set up performance budgets
  • [ ] Use React DevTools Profiler
  • [ ] Track bundle size over time