Oreoluwa
React Performance Optimization: Advanced Techniques
June 3, 2024
12 min read

React Performance Optimization: Advanced Techniques

React
Performance
JavaScript
Optimization

Building performant React applications requires understanding and implementing various optimization techniques. This guide covers advanced strategies to make your React apps lightning-fast.

Understanding React Performance

React Rendering Process

React's rendering process involves several phases:

  1. Trigger: State change or prop update
  2. Render: Component re-execution and virtual DOM creation
  3. Commit: DOM updates and effect execution

Performance Measurement Tools

// React DevTools Profiler
import { Profiler } from 'react';

function App() {
  const onRenderCallback = (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
    console.log('Profiler data:', {
      id,
      phase,
      actualDuration,
      baseDuration,
      startTime,
      commitTime
    });
  };

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

// Performance API
function measurePerformance(name, fn) {
  return (...args) => {
    performance.mark(`${name}-start`);
    const result = fn(...args);
    performance.mark(`${name}-end`);
    performance.measure(name, `${name}-start`, `${name}-end`);
    
    const measure = performance.getEntriesByName(name)[0];
    console.log(`${name} took ${measure.duration} milliseconds`);
    
    return result;
  };
}

// Web Vitals measurement
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';

function sendToAnalytics(metric) {
  // Send to your analytics service
  console.log(metric);
}

getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);

Memoization Strategies

React.memo for Component Memoization

import React, { memo, useState, useMemo, useCallback } from 'react';

// Basic memoization
const ExpensiveComponent = memo(({ data, onUpdate }) => {
  console.log('ExpensiveComponent rendering');
  
  return (
    <div>
      {data.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
      <button onClick={onUpdate}>Update</button>
    </div>
  );
});

// Custom comparison function
const SmartComponent = memo(({ user, settings }) => {
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{settings.theme}</p>
    </div>
  );
}, (prevProps, nextProps) => {
  // Only re-render if user.name or settings.theme changes
  return (
    prevProps.user.name === nextProps.user.name &&
    prevProps.settings.theme === nextProps.settings.theme
  );
});

// Parent component with optimized callbacks
function ParentComponent() {
  const [data, setData] = useState([]);
  const [count, setCount] = useState(0);
  
  // Memoized callback to prevent unnecessary re-renders
  const handleUpdate = useCallback(() => {
    setData(prevData => [...prevData, { id: Date.now(), name: `Item ${Date.now()}` }]);
  }, []);
  
  // Expensive calculation memoized
  const expensiveValue = useMemo(() => {
    console.log('Calculating expensive value');
    return data.reduce((sum, item) => sum + item.id, 0);
  }, [data]);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <p>Expensive value: {expensiveValue}</p>
      <ExpensiveComponent data={data} onUpdate={handleUpdate} />
    </div>
  );
}

Advanced useMemo Patterns

import { useMemo, useRef, useEffect } from 'react';

// Memoizing complex objects
function useComplexCalculation(dependencies) {
  return useMemo(() => {
    // Expensive calculation
    const result = dependencies.reduce((acc, dep) => {
      return {
        ...acc,
        [dep.key]: dep.value * 2 + Math.random()
      };
    }, {});
    
    return result;
  }, [dependencies]);
}

// Memoizing with deep comparison
function useDeepMemo(value, deps) {
  const ref = useRef();
  const signalRef = useRef(0);
  
  const isEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);
  
  if (!isEqual(value, ref.current)) {
    ref.current = value;
    signalRef.current += 1;
  }
  
  return useMemo(() => ref.current, [signalRef.current, ...deps]);
}

// Selective memoization based on conditions
function ConditionalMemoComponent({ data, shouldOptimize }) {
  const processedData = useMemo(() => {
    if (!shouldOptimize) return data;
    
    console.log('Processing data...');
    return data
      .filter(item => item.active)
      .sort((a, b) => a.priority - b.priority)
      .map(item => ({ ...item, processed: true }));
  }, shouldOptimize ? [data] : []);
  
  return (
    <div>
      {(shouldOptimize ? processedData : data).map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}

Code Splitting and Lazy Loading

Dynamic Imports and React.lazy

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

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

// Component-level code splitting
function App() {
  const [currentView, setCurrentView] = useState('home');
  
  return (
    <div>
      <nav>
        <button onClick={() => setCurrentView('dashboard')}>Dashboard</button>
        <button onClick={() => setCurrentView('profile')}>Profile</button>
      </nav>
      
      <Suspense fallback={<div>Loading...</div>}>
        {currentView === 'dashboard' && <Dashboard />}
        {currentView === 'profile' && <Profile />}
      </Suspense>
    </div>
  );
}

// Route-based code splitting with React Router
import { BrowserRouter, Routes, Route } from 'react-router-dom';

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));

function AppRouter() {
  return (
    <BrowserRouter>
      <Suspense fallback={<LoadingSpinner />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/contact" element={<Contact />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

// Preloading strategies
function usePreloadRoute(routeComponent) {
  useEffect(() => {
    const preload = () => {
      // Preload on hover or other interactions
      routeComponent();
    };
    
    return preload;
  }, [routeComponent]);
}

// Smart loading with error boundaries
class LazyErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    console.error('Lazy loading error:', error, errorInfo);
  }
  
  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h2>Failed to load component</h2>
          <button onClick={() => this.setState({ hasError: false })}>
            Retry
          </button>
        </div>
      );
    }
    
    return this.props.children;
  }
}

Bundle Analysis and Optimization

// webpack-bundle-analyzer configuration
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      openAnalyzer: false,
    })
  ],
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10,
          enforce: true,
        },
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

// Dynamic import with error handling
async function loadModule(moduleName) {
  try {
    const module = await import(/* webpackChunkName: "[request]" */ `./modules/${moduleName}`);
    return module.default;
  } catch (error) {
    console.error(`Failed to load module ${moduleName}:`, error);
    // Fallback or retry logic
    return null;
  }
}

Virtual Scrolling and Large Lists

Implementing Virtual Scrolling

import React, { useState, useEffect, useRef, useMemo } from 'react';

function VirtualList({ items, itemHeight, containerHeight, renderItem }) {
  const [scrollTop, setScrollTop] = useState(0);
  const scrollElementRef = useRef();
  
  const visibleItems = useMemo(() => {
    const containerScrollTop = scrollTop;
    const startIndex = Math.floor(containerScrollTop / itemHeight);
    const endIndex = Math.min(
      startIndex + Math.ceil(containerHeight / itemHeight) + 1,
      items.length
    );
    
    return items.slice(startIndex, endIndex).map((item, index) => ({
      ...item,
      index: startIndex + index,
    }));
  }, [scrollTop, items, itemHeight, containerHeight]);
  
  const totalHeight = items.length * itemHeight;
  const offsetY = Math.floor(scrollTop / itemHeight) * itemHeight;
  
  return (
    <div
      ref={scrollElementRef}
      style={{ height: containerHeight, overflowY: 'auto' }}
      onScroll={(e) => setScrollTop(e.target.scrollTop)}
    >
      <div style={{ height: totalHeight, position: 'relative' }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map((item) => (
            <div
              key={item.index}
              style={{ height: itemHeight }}
            >
              {renderItem(item, item.index)}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

// Usage with react-window (recommended library)
import { FixedSizeList as List } from 'react-window';

function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      <div style={{ padding: '10px' }}>
        {items[index].name} - {items[index].description}
      </div>
    </div>
  );
  
  return (
    <List
      height={400}
      itemCount={items.length}
      itemSize={60}
      width="100%"
    >
      {Row}
    </List>
  );
}

// Infinite scrolling with virtual list
function InfiniteVirtualList({ loadMore, hasNextPage, isLoading }) {
  const [items, setItems] = useState([]);
  
  const loadMoreItems = useCallback(async () => {
    if (!isLoading && hasNextPage) {
      const newItems = await loadMore();
      setItems(prev => [...prev, ...newItems]);
    }
  }, [loadMore, hasNextPage, isLoading]);
  
  const Row = ({ index, style }) => {
    const item = items[index];
    
    // Load more when reaching near the end
    if (index === items.length - 5) {
      loadMoreItems();
    }
    
    return (
      <div style={style}>
        {item ? (
          <div>{item.name}</div>
        ) : (
          <div>Loading...</div>
        )}
      </div>
    );
  };
  
  return (
    <List
      height={400}
      itemCount={hasNextPage ? items.length + 1 : items.length}
      itemSize={60}
      width="100%"
    >
      {Row}
    </List>
  );
}

State Management Optimization

Reducing Re-renders with State Structure

// Poor state structure - causes unnecessary re-renders
function BadExample() {
  const [state, setState] = useState({
    user: { name: '', email: '' },
    posts: [],
    comments: [],
    ui: { loading: false, error: null }
  });
  
  // This re-renders everything when loading changes
  const setLoading = (loading) => {
    setState(prev => ({ ...prev, ui: { ...prev.ui, loading } }));
  };
  
  return <div>...</div>;
}

// Better state structure - granular updates
function GoodExample() {
  const [user, setUser] = useState({ name: '', email: '' });
  const [posts, setPosts] = useState([]);
  const [comments, setComments] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  return <div>...</div>;
}

// Context optimization with multiple contexts
const UserContext = createContext();
const PostsContext = createContext();
const UIContext = createContext();

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  const [ui, setUI] = useState({ loading: false, error: null });
  
  return (
    <UserContext.Provider value={{ user, setUser }}>
      <PostsContext.Provider value={{ posts, setPosts }}>
        <UIContext.Provider value={{ ui, setUI }}>
          {children}
        </UIContext.Provider>
      </PostsContext.Provider>
    </UserContext.Provider>
  );
}

// Zustand for optimized state management
import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';

const useStore = create(
  subscribeWithSelector((set, get) => ({
    user: null,
    posts: [],
    comments: [],
    
    setUser: (user) => set({ user }),
    addPost: (post) => set((state) => ({ posts: [...state.posts, post] })),
    
    // Computed values
    get userPosts() {
      const { user, posts } = get();
      return posts.filter(post => post.authorId === user?.id);
    },
  }))
);

// Selective subscriptions
function UserProfile() {
  const user = useStore((state) => state.user);
  const setUser = useStore((state) => state.setUser);
  
  // This component only re-renders when user changes
  return <div>{user?.name}</div>;
}

Image and Asset Optimization

Lazy Loading Images

import React, { useState, useRef, useEffect } from 'react';

function LazyImage({ src, alt, placeholder, ...props }) {
  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();
        }
      },
      { threshold: 0.1 }
    );
    
    if (imgRef.current) {
      observer.observe(imgRef.current);
    }
    
    return () => observer.disconnect();
  }, []);
  
  return (
    <div ref={imgRef} {...props}>
      {isInView && (
        <img
          src={src}
          alt={alt}
          onLoad={() => setIsLoaded(true)}
          style={{
            opacity: isLoaded ? 1 : 0,
            transition: 'opacity 0.3s ease-in-out',
          }}
        />
      )}
      {!isLoaded && placeholder && (
        <div style={{ backgroundColor: '#f0f0f0', ...props.style }}>
          {placeholder}
        </div>
      )}
    </div>
  );
}

// Progressive image loading
function ProgressiveImage({ lowQualitySrc, highQualitySrc, alt }) {
  const [currentSrc, setCurrentSrc] = useState(lowQualitySrc);
  const [isHighQualityLoaded, setIsHighQualityLoaded] = useState(false);
  
  useEffect(() => {
    const img = new Image();
    img.onload = () => {
      setCurrentSrc(highQualitySrc);
      setIsHighQualityLoaded(true);
    };
    img.src = highQualitySrc;
  }, [highQualitySrc]);
  
  return (
    <img
      src={currentSrc}
      alt={alt}
      style={{
        filter: isHighQualityLoaded ? 'none' : 'blur(2px)',
        transition: 'filter 0.3s ease-in-out',
      }}
    />
  );
}

// WebP support detection
function OptimizedImage({ src, webpSrc, alt, ...props }) {
  const [supportsWebP, setSupportsWebP] = useState(false);
  
  useEffect(() => {
    const checkWebPSupport = () => {
      const canvas = document.createElement('canvas');
      canvas.width = 1;
      canvas.height = 1;
      return canvas.toDataURL('image/webp').indexOf('image/webp') === 5;
    };
    
    setSupportsWebP(checkWebPSupport());
  }, []);
  
  return (
    <img
      src={supportsWebP && webpSrc ? webpSrc : src}
      alt={alt}
      {...props}
    />
  );
}

Advanced Patterns

Render Props and Higher-Order Components

// Optimized render props pattern
function DataFetcher({ url, children, dependencies = [] }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  const fetchData = useCallback(async () => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await fetch(url);
      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err);
    } finally {
      setLoading(false);
    }
  }, [url, ...dependencies]);
  
  useEffect(() => {
    fetchData();
  }, [fetchData]);
  
  return children({ data, loading, error, refetch: fetchData });
}

// Memoized HOC
function withMemoization(WrappedComponent, propsAreEqual) {
  const MemoizedComponent = memo(WrappedComponent, propsAreEqual);
  
  return function WithMemoizationComponent(props) {
    return <MemoizedComponent {...props} />;
  };
}

// Performance monitoring HOC
function withPerformanceMonitoring(WrappedComponent, componentName) {
  return function PerformanceMonitoredComponent(props) {
    const renderStartTime = useRef();
    
    renderStartTime.current = performance.now();
    
    useEffect(() => {
      const renderEndTime = performance.now();
      const renderDuration = renderEndTime - renderStartTime.current;
      
      if (renderDuration > 16) { // Longer than one frame
        console.warn(`${componentName} took ${renderDuration}ms to render`);
      }
    });
    
    return <WrappedComponent {...props} />;
  };
}

Custom Optimization Hooks

// Debounced state
function useDebouncedState(initialValue, delay) {
  const [value, setValue] = useState(initialValue);
  const [debouncedValue, setDebouncedValue] = useState(initialValue);
  
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    
    return () => clearTimeout(timer);
  }, [value, delay]);
  
  return [debouncedValue, setValue];
}

// Throttled callback
function useThrottledCallback(callback, delay) {
  const throttledCallback = useRef(null);
  
  return useCallback((...args) => {
    if (!throttledCallback.current) {
      callback(...args);
      throttledCallback.current = setTimeout(() => {
        throttledCallback.current = null;
      }, delay);
    }
  }, [callback, delay]);
}

// Intersection observer hook
function useIntersectionObserver(options = {}) {
  const [isIntersecting, setIsIntersecting] = useState(false);
  const [node, setNode] = useState(null);
  
  useEffect(() => {
    if (!node) return;
    
    const observer = new IntersectionObserver(
      ([entry]) => setIsIntersecting(entry.isIntersecting),
      options
    );
    
    observer.observe(node);
    
    return () => observer.disconnect();
  }, [node, options]);
  
  return [setNode, isIntersecting];
}

Production Optimization Checklist

Build Optimization

// Next.js optimization
module.exports = {
  // Image optimization
  images: {
    domains: ['example.com'],
    formats: ['image/webp', 'image/avif'],
  },
  
  // Bundle analyzer
  webpack: (config, { isServer }) => {
    if (process.env.ANALYZE) {
      const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
      config.plugins.push(new BundleAnalyzerPlugin());
    }
    
    return config;
  },
  
  // Experimental features
  experimental: {
    optimizeCss: true,
    modern: true,
  },
};

// Vite optimization
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          router: ['react-router-dom'],
          ui: ['@mui/material'],
        },
      },
    },
  },
  plugins: [
    react(),
    splitVendorChunkPlugin(),
  ],
});

Conclusion

React performance optimization is crucial for delivering excellent user experiences. Key strategies include:

  1. Memoization: Use React.memo, useMemo, and useCallback strategically
  2. Code Splitting: Implement lazy loading and route-based splitting
  3. Virtual Scrolling: Handle large lists efficiently
  4. State Optimization: Structure state to minimize re-renders
  5. Asset Optimization: Lazy load images and use modern formats
  6. Monitoring: Continuously measure and monitor performance

Remember that premature optimization can be counterproductive. Always measure first, identify bottlenecks, then apply appropriate optimization techniques. Use React DevTools Profiler to identify performance issues and validate your optimizations.