DEV Community

Cover image for React Performance Optimization: Advanced Techniques for Lightning-Fast Apps
Sepehr Mohseni
Sepehr Mohseni

Posted on

React Performance Optimization: Advanced Techniques for Lightning-Fast Apps

Performance is crucial for user experience and SEO. A slow React application can lead to higher bounce rates and lower conversion. This guide covers advanced techniques to optimize your React applications for maximum performance.

Understanding React's Rendering Behavior

Before optimizing, understand how React decides to re-render components:

// Every state change triggers a re-render of this component
// AND all its children
function ParentComponent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>
        Count: {count}
      </button>
      <ExpensiveChild /> {/* Re-renders even though it doesn't use count! */}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode


React re-renders all child components when a parent re-renders, regardless of whether their props changed. This is the primary source of performance issues.

Memoization Techniques

React.memo for Component Memoization

Prevent unnecessary re-renders with React.memo:

import { memo } from 'react';

// Without memo - re-renders on every parent render
function ProductCard({ product, onAddToCart }) {
  console.log('ProductCard rendered');
  return (
    <div className="product-card">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <button onClick={() => onAddToCart(product.id)}>
        Add to Cart
      </button>
    </div>
  );
}

// With memo - only re-renders when props change
const MemoizedProductCard = memo(ProductCard);

// Custom comparison function for complex props
const MemoizedProductCardCustom = memo(ProductCard, (prevProps, nextProps) => {
  return prevProps.product.id === nextProps.product.id &&
         prevProps.product.price === nextProps.product.price;
});
Enter fullscreen mode Exit fullscreen mode

useMemo for Expensive Calculations

import { useMemo } from 'react';

function ProductList({ products, filters }) {
  // Without useMemo - recalculates on every render
  // const filteredProducts = products.filter(p => 
  //   p.category === filters.category && p.price <= filters.maxPrice
  // );

  // With useMemo - only recalculates when dependencies change
  const filteredProducts = useMemo(() => {
    console.log('Filtering products...');
    return products.filter(p => 
      p.category === filters.category && 
      p.price <= filters.maxPrice
    );
  }, [products, filters.category, filters.maxPrice]);

  // Expensive sorting operation
  const sortedProducts = useMemo(() => {
    return [...filteredProducts].sort((a, b) => {
      if (filters.sortBy === 'price') return a.price - b.price;
      if (filters.sortBy === 'name') return a.name.localeCompare(b.name);
      return 0;
    });
  }, [filteredProducts, filters.sortBy]);

  return (
    <div className="product-grid">
      {sortedProducts.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

useCallback for Stable Function References

import { useCallback, useState } from 'react';

function ShoppingCart() {
  const [items, setItems] = useState([]);

  // Without useCallback - new function on every render
  // const handleRemove = (id) => {
  //   setItems(items => items.filter(item => item.id !== id));
  // };

  // With useCallback - stable reference
  const handleRemove = useCallback((id) => {
    setItems(items => items.filter(item => item.id !== id));
  }, []); // Empty deps - function never changes

  const handleUpdateQuantity = useCallback((id, quantity) => {
    setItems(items => items.map(item => 
      item.id === id ? { ...item, quantity } : item
    ));
  }, []);

  return (
    <div>
      {items.map(item => (
        <CartItem
          key={item.id}
          item={item}
          onRemove={handleRemove}
          onUpdateQuantity={handleUpdateQuantity}
        />
      ))}
    </div>
  );
}

// CartItem can now be memoized effectively
const CartItem = memo(function CartItem({ item, onRemove, onUpdateQuantity }) {
  return (
    <div className="cart-item">
      <span>{item.name}</span>
      <input
        type="number"
        value={item.quantity}
        onChange={(e) => onUpdateQuantity(item.id, parseInt(e.target.value))}
      />
      <button onClick={() => onRemove(item.id)}>Remove</button>
    </div>
  );
});
Enter fullscreen mode Exit fullscreen mode


Only use useMemo and useCallback when you have measured a performance problem. Premature optimization can make code harder to read without meaningful benefits.

Code Splitting and Lazy Loading

Route-Based Code Splitting

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

// Lazy load route components
const Home = lazy(() => import('./pages/Home'));
const Products = lazy(() => import('./pages/Products'));
const ProductDetail = lazy(() => import('./pages/ProductDetail'));
const Checkout = lazy(() => import('./pages/Checkout'));
const Admin = lazy(() => import('./pages/Admin'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<PageLoader />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/products" element={<Products />} />
          <Route path="/products/:id" element={<ProductDetail />} />
          <Route path="/checkout" element={<Checkout />} />
          <Route path="/admin/*" element={<Admin />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

function PageLoader() {
  return (
    <div className="flex items-center justify-center min-h-screen">
      <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-500" />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Component-Level Code Splitting

import { lazy, Suspense, useState } from 'react';

// Heavy components loaded on demand
const RichTextEditor = lazy(() => import('./components/RichTextEditor'));
const ChartDashboard = lazy(() => import('./components/ChartDashboard'));
const ImageGallery = lazy(() => import('./components/ImageGallery'));

function ProductEditor({ product }) {
  const [showCharts, setShowCharts] = useState(false);

  return (
    <div>
      <h1>Edit Product</h1>

      {/* Editor loads when component mounts */}
      <Suspense fallback={<div>Loading editor...</div>}>
        <RichTextEditor 
          content={product.description}
          onChange={handleDescriptionChange}
        />
      </Suspense>

      {/* Charts only load when user clicks */}
      <button onClick={() => setShowCharts(true)}>
        Show Analytics
      </button>

      {showCharts && (
        <Suspense fallback={<div>Loading charts...</div>}>
          <ChartDashboard productId={product.id} />
        </Suspense>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Virtualization for Large Lists

Using react-window for Efficient Rendering

import { FixedSizeList as List } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';

function VirtualizedProductList({ products }) {
  const Row = ({ index, style }) => {
    const product = products[index];
    return (
      <div style={style} className="flex items-center p-4 border-b">
        <img 
          src={product.thumbnail} 
          alt={product.name}
          className="w-16 h-16 object-cover rounded"
        />
        <div className="ml-4">
          <h3 className="font-semibold">{product.name}</h3>
          <p className="text-gray-600">${product.price}</p>
        </div>
      </div>
    );
  };

  return (
    <div className="h-[600px] w-full">
      <AutoSizer>
        {({ height, width }) => (
          <List
            height={height}
            width={width}
            itemCount={products.length}
            itemSize={80}
          >
            {Row}
          </List>
        )}
      </AutoSizer>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Variable Size List for Dynamic Content

import { VariableSizeList as List } from 'react-window';
import { useRef, useCallback } from 'react';

function ChatMessages({ messages }) {
  const listRef = useRef();
  const rowHeights = useRef({});

  const getRowHeight = useCallback((index) => {
    return rowHeights.current[index] || 60;
  }, []);

  const setRowHeight = useCallback((index, size) => {
    rowHeights.current[index] = size;
    listRef.current?.resetAfterIndex(index);
  }, []);

  const Row = ({ index, style }) => {
    const rowRef = useRef();
    const message = messages[index];

    useEffect(() => {
      if (rowRef.current) {
        setRowHeight(index, rowRef.current.getBoundingClientRect().height);
      }
    }, [index, setRowHeight]);

    return (
      <div style={style}>
        <div ref={rowRef} className="p-4">
          <p className="font-semibold">{message.sender}</p>
          <p>{message.content}</p>
          <span className="text-xs text-gray-500">{message.timestamp}</span>
        </div>
      </div>
    );
  };

  return (
    <List
      ref={listRef}
      height={500}
      width="100%"
      itemCount={messages.length}
      itemSize={getRowHeight}
      estimatedItemSize={60}
    >
      {Row}
    </List>
  );
}
Enter fullscreen mode Exit fullscreen mode

State Management Optimization

Avoiding Unnecessary Context Re-renders

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

// Split context by update frequency
const UserContext = createContext();
const UserActionsContext = createContext();

function UserProvider({ children }) {
  const [user, setUser] = useState(null);

  // Memoize actions to prevent re-renders
  const actions = useMemo(() => ({
    login: async (credentials) => {
      const user = await authService.login(credentials);
      setUser(user);
    },
    logout: () => {
      authService.logout();
      setUser(null);
    },
    updateProfile: async (data) => {
      const updated = await userService.update(data);
      setUser(updated);
    },
  }), []);

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

// Components only subscribe to what they need
function UserAvatar() {
  const user = useContext(UserContext); // Only re-renders when user changes
  return <img src={user?.avatar} alt={user?.name} />;
}

function LogoutButton() {
  const { logout } = useContext(UserActionsContext); // Never re-renders!
  return <button onClick={logout}>Logout</button>;
}
Enter fullscreen mode Exit fullscreen mode

Using Zustand for Efficient State

import { create } from 'zustand';
import { shallow } from 'zustand/shallow';

const useStore = create((set, get) => ({
  products: [],
  cart: [],
  filters: { category: 'all', maxPrice: 1000 },

  setProducts: (products) => set({ products }),

  addToCart: (product) => set((state) => ({
    cart: [...state.cart, { ...product, quantity: 1 }]
  })),

  updateFilters: (filters) => set((state) => ({
    filters: { ...state.filters, ...filters }
  })),

  // Computed values
  get cartTotal() {
    return get().cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
  },
}));

// Select only needed state to prevent unnecessary re-renders
function CartIcon() {
  const itemCount = useStore((state) => state.cart.length);
  return <span className="badge">{itemCount}</span>;
}

function ProductFilters() {
  // Use shallow comparison for object selections
  const { filters, updateFilters } = useStore(
    (state) => ({ filters: state.filters, updateFilters: state.updateFilters }),
    shallow
  );

  return (
    <div>
      <select 
        value={filters.category}
        onChange={(e) => updateFilters({ category: e.target.value })}
      >
        <option value="all">All Categories</option>
        <option value="electronics">Electronics</option>
        <option value="clothing">Clothing</option>
      </select>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Image Optimization

Lazy Loading Images

function OptimizedImage({ src, alt, className }) {
  const [isLoaded, setIsLoaded] = useState(false);
  const [isInView, setIsInView] = useState(false);
  const imgRef = useRef();

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsInView(true);
          observer.disconnect();
        }
      },
      { rootMargin: '100px' }
    );

    if (imgRef.current) {
      observer.observe(imgRef.current);
    }

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

  return (
    <div ref={imgRef} className={`relative ${className}`}>
      {/* Placeholder */}
      {!isLoaded && (
        <div className="absolute inset-0 bg-gray-200 animate-pulse" />
      )}

      {/* Actual image */}
      {isInView && (
        <img
          src={src}
          alt={alt}
          className={`transition-opacity duration-300 ${
            isLoaded ? 'opacity-100' : 'opacity-0'
          }`}
          onLoad={() => setIsLoaded(true)}
          loading="lazy"
        />
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Profiling and Measuring Performance

Using React DevTools Profiler

import { Profiler } from 'react';

function onRenderCallback(
  id,
  phase,
  actualDuration,
  baseDuration,
  startTime,
  commitTime
) {
  // Log slow renders
  if (actualDuration > 16) { // More than one frame (60fps)
    console.warn(`Slow render in ${id}:`, {
      phase,
      actualDuration: `${actualDuration.toFixed(2)}ms`,
      baseDuration: `${baseDuration.toFixed(2)}ms`,
    });
  }
}

function App() {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <Header />
      <Profiler id="ProductList" onRender={onRenderCallback}>
        <ProductList />
      </Profiler>
      <Footer />
    </Profiler>
  );
}
Enter fullscreen mode Exit fullscreen mode

Custom Performance Hooks

function useRenderCount(componentName) {
  const renderCount = useRef(0);

  useEffect(() => {
    renderCount.current += 1;
    console.log(`${componentName} rendered ${renderCount.current} times`);
  });
}

function useWhyDidYouUpdate(name, props) {
  const previousProps = useRef();

  useEffect(() => {
    if (previousProps.current) {
      const allKeys = Object.keys({ ...previousProps.current, ...props });
      const changedProps = {};

      allKeys.forEach(key => {
        if (previousProps.current[key] !== props[key]) {
          changedProps[key] = {
            from: previousProps.current[key],
            to: props[key],
          };
        }
      });

      if (Object.keys(changedProps).length) {
        console.log(`[${name}] Changed props:`, changedProps);
      }
    }

    previousProps.current = props;
  });
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

React performance optimization is about understanding the rendering lifecycle and applying the right techniques at the right time. Start by measuring with React DevTools Profiler, identify bottlenecks, and apply targeted optimizations.

Key takeaways:

  • Use React.memo, useMemo, and useCallback strategically
  • Implement code splitting for large applications
  • Virtualize long lists
  • Optimize state management to minimize re-renders
  • Always measure before and after optimization

Top comments (0)