React Performance Optimization: Advanced Techniques

Deep dive into advanced React performance optimization techniques including memoization, code splitting, and virtual scrolling.

Nosgnoh
November 20, 2024
5 minute read
React Performance Optimization: Advanced Techniques

React Performance Optimization: Advanced Techniques

Performance optimization in React applications is crucial for delivering excellent user experiences. Let's explore advanced techniques that can dramatically improve your app's performance.

Understanding React's Rendering Process

Before optimizing, it's important to understand when and why React re-renders components:

// This component re-renders whenever the parent re-renders
function ExpensiveComponent({ data }) {
  const expensiveValue = computeExpensiveValue(data);
  
  return <div>{expensiveValue}</div>;
}

Memoization Techniques

React.memo for Component Memoization

import React, { memo } from 'react';

const ExpensiveComponent = memo(({ data, onUpdate }) => {
  const expensiveValue = computeExpensiveValue(data);
  
  return (
    <div onClick={() => onUpdate(data.id)}>
      {expensiveValue}
    </div>
  );
}, (prevProps, nextProps) => {
  // Custom comparison function
  return prevProps.data.id === nextProps.data.id;
});

useMemo for Expensive Calculations

import { useMemo } from 'react';

function DataVisualization({ rawData, filters }) {
  const processedData = useMemo(() => {
    return rawData
      .filter(item => filters.includes(item.category))
      .map(item => transformData(item))
      .sort((a, b) => b.value - a.value);
  }, [rawData, filters]);

  return <Chart data={processedData} />;
}

useCallback for Function Memoization

import { useCallback, useState } from 'react';

function TodoList({ todos }) {
  const [filter, setFilter] = useState('all');

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

  const filteredTodos = useMemo(() => {
    return todos.filter(todo => {
      if (filter === 'completed') return todo.completed;
      if (filter === 'active') return !todo.completed;
      return true;
    });
  }, [todos, filter]);

  return (
    <div>
      {filteredTodos.map(todo => (
        <TodoItem 
          key={todo.id} 
          todo={todo} 
          onToggle={handleToggle} 
        />
      ))}
    </div>
  );
}

Code Splitting and Lazy Loading

Component-Level Code Splitting

import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));
const Dashboard = lazy(() => import('./Dashboard'));

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

Dynamic Imports with Conditions

import { useState } from 'react';

function AdvancedEditor() {
  const [showAdvanced, setShowAdvanced] = useState(false);
  const [AdvancedEditor, setAdvancedEditor] = useState(null);

  const loadAdvancedEditor = async () => {
    if (!AdvancedEditor) {
      const module = await import('./AdvancedEditor');
      setAdvancedEditor(() => module.default);
    }
    setShowAdvanced(true);
  };

  return (
    <div>
      <BasicEditor />
      {!showAdvanced && (
        <button onClick={loadAdvancedEditor}>
          Load Advanced Editor
        </button>
      )}
      {showAdvanced && AdvancedEditor && <AdvancedEditor />}
    </div>
  );
}

Virtual Scrolling for Large Lists

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>
  );
}

Image Optimization

Next.js Image Component

import Image from 'next/image';

function OptimizedGallery({ photos }) {
  return (
    <div className="grid grid-cols-3 gap-4">
      {photos.map((photo) => (
        <Image
          key={photo.id}
          src={photo.src}
          alt={photo.alt}
          width={400}
          height={300}
          loading="lazy"
          placeholder="blur"
          blurDataURL="data:image/jpeg;base64,..."
        />
      ))}
    </div>
  );
}

Progressive Image Loading

import { useState } from 'react';

function ProgressiveImage({ src, placeholder, alt }) {
  const [loaded, setLoaded] = useState(false);

  return (
    <div className="relative">
      <img
        src={placeholder}
        alt={alt}
        className={`transition-opacity ${loaded ? 'opacity-0' : 'opacity-100'}`}
      />
      <img
        src={src}
        alt={alt}
        className={`absolute inset-0 transition-opacity ${loaded ? 'opacity-100' : 'opacity-0'}`}
        onLoad={() => setLoaded(true)}
      />
    </div>
  );
}

State Management Optimization

Context Splitting

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

function AppProviders({ children }) {
  return (
    <UserProvider>
      <ThemeProvider>
        <NotificationProvider>
          {children}
        </NotificationProvider>
      </ThemeProvider>
    </UserProvider>
  );
}

Reducer with Immer

import { useReducer } from 'react';
import { produce } from 'immer';

const todoReducer = produce((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(t => t.id === action.id);
      if (todo) {
        todo.completed = !todo.completed;
      }
      break;
  }
});

Performance Monitoring

Custom Performance Hook

import { useEffect, useRef } from 'react';

function usePerformanceMonitor(name) {
  const renderStart = useRef(performance.now());

  useEffect(() => {
    const renderTime = performance.now() - renderStart.current;
    console.log(`${name} render time: ${renderTime.toFixed(2)}ms`);
  });

  useEffect(() => {
    renderStart.current = performance.now();
  });
}

function ExpensiveComponent() {
  usePerformanceMonitor('ExpensiveComponent');
  
  // Component logic here
  return <div>Expensive Component</div>;
}

React DevTools Profiler

import { Profiler } from 'react';

function onRenderCallback(id, phase, actualDuration) {
  console.log(`Component ${id} ${phase} phase took ${actualDuration}ms`);
}

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

Best Practices Summary

  1. Measure First: Use React DevTools Profiler to identify bottlenecks
  2. Avoid Premature Optimization: Only optimize components that actually need it
  3. Use Memoization Wisely: Don't memoize everything; it has overhead
  4. Split Large Components: Keep components focused and small
  5. Optimize Bundle Size: Use code splitting and tree shaking
  6. Monitor Performance: Set up performance budgets and monitoring

Conclusion

React performance optimization is an ongoing process. Start with measuring your app's performance, identify bottlenecks, and apply these techniques strategically. Remember, the best optimization is often avoiding unnecessary work altogether.

By implementing these advanced techniques, you can build React applications that are not only feature-rich but also incredibly fast and responsive.

Tags

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