React Performance Optimization: Advanced Techniques for 2024

Master advanced React performance optimization techniques including memoization, code splitting, virtual scrolling, and server-side rendering strategies.

Nosgnoh
February 20, 2024
7 minute read
React Performance Optimization: Advanced Techniques for 2024

React Performance Optimization: Advanced Techniques for 2024

Performance is crucial for user experience, and React applications can benefit significantly from proper optimization techniques. This comprehensive guide covers advanced strategies to make your React apps lightning-fast.

Understanding React Performance

React's performance is primarily affected by:

  • Re-renders: Unnecessary component updates
  • Bundle size: Large JavaScript bundles slow initial load
  • Network requests: Inefficient data fetching
  • Memory usage: Leaks and excessive object creation

Memoization Strategies

React.memo for Component Memoization

// ✅ Optimized component with React.memo
const ExpensiveComponent = React.memo(({ data, onUpdate }) => {
  console.log('ExpensiveComponent rendered');
  
  return (
    <div>
      {data.map(item => (
        <ComplexItem key={item.id} item={item} onUpdate={onUpdate} />
      ))}
    </div>
  );
});

// ✅ Custom comparison function
const OptimizedComponent = React.memo(({ user, settings }) => {
  return <UserProfile user={user} settings={settings} />;
}, (prevProps, nextProps) => {
  // Only re-render if user ID changes
  return prevProps.user.id === nextProps.user.id;
});

useMemo for Expensive Calculations

function DataVisualization({ rawData, filters }) {
  // ✅ Memoize expensive calculations
  const processedData = useMemo(() => {
    console.log('Processing data...');
    return rawData
      .filter(item => filters.includes(item.category))
      .map(item => ({
        ...item,
        processedValue: expensiveCalculation(item.value)
      }))
      .sort((a, b) => b.processedValue - a.processedValue);
  }, [rawData, filters]);

  const chartConfig = useMemo(() => ({
    type: 'line',
    data: processedData,
    options: {
      responsive: true,
      plugins: {
        legend: { position: 'top' }
      }
    }
  }), [processedData]);

  return <Chart {...chartConfig} />;
}

useCallback for Function References

function ParentComponent({ items }) {
  const [filter, setFilter] = useState('');
  const [sortOrder, setSortOrder] = useState('asc');

  // ✅ Memoize callback functions
  const handleItemClick = useCallback((itemId) => {
    console.log('Item clicked:', itemId);
    // Handle item click logic
  }, []);

  const handleSort = useCallback((newSortOrder) => {
    setSortOrder(newSortOrder);
  }, []);

  const filteredItems = useMemo(() => 
    items.filter(item => 
      item.name.toLowerCase().includes(filter.toLowerCase())
    ), [items, filter]
  );

  return (
    <div>
      <SearchInput value={filter} onChange={setFilter} />
      <SortControls onSort={handleSort} />
      <ItemList 
        items={filteredItems} 
        onItemClick={handleItemClick} 
      />
    </div>
  );
}

Code Splitting and Lazy Loading

Route-Based Code Splitting

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

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

function App() {
  return (
    <Routes>
      <Route path="/" element={
        <Suspense fallback={<PageSkeleton />}>
          <Dashboard />
        </Suspense>
      } />
      <Route path="/profile" element={
        <Suspense fallback={<PageSkeleton />}>
          <Profile />
        </Suspense>
      } />
      <Route path="/analytics" element={
        <Suspense fallback={<AnalyticsSkeleton />}>
          <Analytics />
        </Suspense>
      } />
    </Routes>
  );
}

Component-Based Code Splitting

// ✅ Split heavy components
const HeavyChart = lazy(() => 
  import('./components/HeavyChart').then(module => ({
    default: module.HeavyChart
  }))
);

const HeavyTable = lazy(() => import('./components/HeavyTable'));

function Dashboard({ showChart, showTable }) {
  return (
    <div>
      <h1>Dashboard</h1>
      
      {showChart && (
        <Suspense fallback={<ChartSkeleton />}>
          <HeavyChart />
        </Suspense>
      )}
      
      {showTable && (
        <Suspense fallback={<TableSkeleton />}>
          <HeavyTable />
        </Suspense>
      )}
    </div>
  );
}

Virtual Scrolling for Large Lists

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

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

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

// ✅ For dynamic heights
import { VariableSizeList as List } from 'react-window';

function DynamicVirtualizedList({ items }) {
  const getItemSize = (index) => {
    // Calculate height based on content
    return items[index].type === 'image' ? 200 : 80;
  };

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

Image Optimization

// ✅ Optimized image component
function OptimizedImage({ src, alt, ...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} className="relative">
      {!isLoaded && <ImageSkeleton />}
      {isInView && (
        <img
          src={src}
          alt={alt}
          onLoad={() => setIsLoaded(true)}
          className={`transition-opacity ${isLoaded ? 'opacity-100' : 'opacity-0'}`}
          {...props}
        />
      )}
    </div>
  );
}

State Management Optimization

Optimizing Context Usage

// ✅ Split contexts by concern
const UserContext = createContext();
const ThemeContext = createContext();
const CartContext = createContext();

// ✅ Memoize context values
function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  
  const value = useMemo(() => ({
    user,
    updateUser: setUser,
    logout: () => setUser(null)
  }), [user]);

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

State Normalization

// ✅ Normalized state structure
const initialState = {
  posts: {
    byId: {},
    allIds: []
  },
  comments: {
    byId: {},
    allIds: []
  },
  users: {
    byId: {},
    allIds: []
  }
};

// ✅ Selectors for denormalized data
const selectPostWithComments = (state, postId) => {
  const post = state.posts.byId[postId];
  const comments = post.commentIds.map(id => state.comments.byId[id]);
  return { ...post, comments };
};

Network Optimization

Request Batching

// ✅ Batch multiple requests
class RequestBatcher {
  constructor(batchSize = 10, delay = 50) {
    this.batchSize = batchSize;
    this.delay = delay;
    this.queue = [];
    this.timeoutId = null;
  }

  add(request) {
    return new Promise((resolve, reject) => {
      this.queue.push({ request, resolve, reject });
      
      if (this.queue.length >= this.batchSize) {
        this.flush();
      } else if (!this.timeoutId) {
        this.timeoutId = setTimeout(() => this.flush(), this.delay);
      }
    });
  }

  async flush() {
    if (this.queue.length === 0) return;
    
    const batch = this.queue.splice(0, this.batchSize);
    this.timeoutId = null;

    try {
      const requests = batch.map(({ request }) => request);
      const responses = await Promise.all(requests);
      
      batch.forEach(({ resolve }, index) => {
        resolve(responses[index]);
      });
    } catch (error) {
      batch.forEach(({ reject }) => reject(error));
    }
  }
}

Caching Strategies

// ✅ Simple cache implementation
class ApiCache {
  constructor(ttl = 300000) { // 5 minutes
    this.cache = new Map();
    this.ttl = ttl;
  }

  set(key, data) {
    this.cache.set(key, {
      data,
      timestamp: Date.now()
    });
  }

  get(key) {
    const item = this.cache.get(key);
    
    if (!item) return null;
    
    if (Date.now() - item.timestamp > this.ttl) {
      this.cache.delete(key);
      return null;
    }
    
    return item.data;
  }
}

// ✅ Custom hook with caching
function useApiData(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const cachedData = apiCache.get(url);
    
    if (cachedData) {
      setData(cachedData);
      setLoading(false);
      return;
    }

    const fetchData = async () => {
      try {
        const response = await fetch(url, options);
        const result = await response.json();
        
        apiCache.set(url, result);
        setData(result);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url, options]);

  return { data, loading, error };
}

Performance Monitoring

Core Web Vitals Tracking

// ✅ Performance monitoring
function usePerformanceMonitoring() {
  useEffect(() => {
    // Largest Contentful Paint
    new PerformanceObserver((entryList) => {
      const entries = entryList.getEntries();
      const lastEntry = entries[entries.length - 1];
      console.log('LCP:', lastEntry.startTime);
    }).observe({ entryTypes: ['largest-contentful-paint'] });

    // First Input Delay
    new PerformanceObserver((entryList) => {
      const entries = entryList.getEntries();
      entries.forEach((entry) => {
        console.log('FID:', entry.processingStart - entry.startTime);
      });
    }).observe({ entryTypes: ['first-input'] });

    // Cumulative Layout Shift
    new PerformanceObserver((entryList) => {
      let clsValue = 0;
      entryList.getEntries().forEach((entry) => {
        if (!entry.hadRecentInput) {
          clsValue += entry.value;
        }
      });
      console.log('CLS:', clsValue);
    }).observe({ entryTypes: ['layout-shift'] });
  }, []);
}

Best Practices Checklist

  • ✅ Use React.memo for expensive components
  • ✅ Implement useMemo for expensive calculations
  • ✅ Use useCallback for event handlers
  • ✅ Split code at route and component levels
  • ✅ Implement virtual scrolling for large lists
  • ✅ Optimize images with lazy loading
  • ✅ Normalize state structure
  • ✅ Batch API requests when possible
  • ✅ Implement caching strategies
  • ✅ Monitor Core Web Vitals

Conclusion

React performance optimization is an ongoing process that requires understanding your application's specific needs. Start with profiling to identify bottlenecks, then apply these techniques systematically.

Remember: measure first, optimize second. Use React DevTools Profiler to identify actual performance issues before applying optimizations.


Want to dive deeper into React performance? Check out my post on Advanced React Patterns for Large Applications for architectural strategies.

Tags

#react#performance#optimization#memoization#code-splitting
All Posts
Share this post with your network