React Performance Optimization Techniques

React Performance Optimization Techniques

React Performance Optimization Techniques

Optimizing React applications is crucial for providing a smooth user experience. This guide covers practical techniques to identify and fix performance issues in your React applications.

Measuring Performance

Before optimizing, you need to measure performance to identify bottlenecks:

React DevTools Profiler

The React DevTools Profiler is your first line of defense:

  1. Install React DevTools browser extension
  2. Open DevTools, navigate to the "Profiler" tab
  3. Click the record button, interact with your app, then stop recording
  4. Analyze component render times and commit frequency

Lighthouse and Web Vitals

Use Lighthouse in Chrome DevTools to measure overall performance metrics like:

  • First Contentful Paint (FCP)
  • Largest Contentful Paint (LCP)
  • Time to Interactive (TTI)
  • Cumulative Layout Shift (CLS)

Component Optimization

Prevent Unnecessary Renders with React.memo

React.memo is a higher-order component that memoizes your component, preventing re-renders if props haven't changed:

const MovieCard = React.memo(function MovieCard({ title, poster, rating }) {
  return (
    <div className="movie-card">
      <img src={poster} alt={title} />
      <h3>{title}</h3>
      <div>Rating: {rating}/10</div>
    </div>
  );
});

Use it for components that:

  • Render often
  • Re-render with the same props
  • Are medium to large in complexity

Custom Comparison Function

You can provide a custom comparison function to React.memo for more control:

function arePropsEqual(prevProps, nextProps) {
  // Only re-render if the rating changed by more than 0.5
  return (
    prevProps.title === nextProps.title &&
    Math.abs(prevProps.rating - nextProps.rating) <= 0.5
  );
}

const MovieCard = React.memo(MovieCardComponent, arePropsEqual);

useMemo for Expensive Calculations

Use useMemo to cache the results of expensive calculations:

function MovieList({ movies, filter }) {
  // This calculation only runs when movies or filter changes
  const filteredMovies = useMemo(() => {
    console.log('Filtering movies...');
    return movies.filter(movie => {
      return movie.title.toLowerCase().includes(filter.toLowerCase());
    });
  }, [movies, filter]);

  return (
    <div>
      {filteredMovies.map(movie => (
        <MovieCard key={movie.id} {...movie} />
      ))}
    </div>
  );
}

useCallback for Stable Function References

Use useCallback to prevent function recreation on each render:

function MovieList({ movies }) {
  const [favorites, setFavorites] = useState([]);

  // This function reference remains stable between renders
  const handleFavorite = useCallback((movieId) => {
    setFavorites(prev => {
      if (prev.includes(movieId)) {
        return prev.filter(id => id !== movieId);
      } else {
        return [...prev, movieId];
      }
    });
  }, []);

  return (
    <div>
      {movies.map(movie => (
        <MovieCard 
          key={movie.id} 
          {...movie} 
          isFavorite={favorites.includes(movie.id)}
          onFavorite={handleFavorite} // Stable reference
        />
      ))}
    </div>
  );
}

State Management Optimization

State Colocation

Keep state as close as possible to where it's used:

// Bad: State is too high in the tree
function App() {
  const [searchTerm, setSearchTerm] = useState('');
  // This causes the entire app to re-render when typing
  
  return (
    <div>
      <Header />
      <SearchBar searchTerm={searchTerm} setSearchTerm={setSearchTerm} />
      <MovieList />
      <Footer />
    </div>
  );
}

// Good: State is colocated with its usage
function App() {
  return (
    <div>
      <Header />
      <SearchFeature /> {/* Contains both SearchBar and filtered results */}
      <Footer />
    </div>
  );
}

function SearchFeature() {
  const [searchTerm, setSearchTerm] = useState('');
  
  return (
    <div>
      <SearchBar searchTerm={searchTerm} setSearchTerm={setSearchTerm} />
      <FilteredMovieList searchTerm={searchTerm} />
    </div>
  );
}

State Normalization

For complex state, normalize your data like a database:

// Before: Nested state that's hard to update
const [state, setState] = useState({
  users: [
    { id: 1, name: 'John', posts: [{ id: 1, title: 'Hello' }] }
  ]
});

// After: Normalized state
const [state, setState] = useState({
  users: {
    1: { id: 1, name: 'John', postIds: [1] }
  },
  posts: {
    1: { id: 1, title: 'Hello', userId: 1 }
  }
});

Rendering Optimization

Virtualization for Long Lists

Use a virtualization library like react-window or react-virtualized for long lists:

import { FixedSizeList } from 'react-window';

function MovieList({ movies }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      <MovieCard {...movies[index]} />
    </div>
  );

  return (
    <FixedSizeList
      height={500}
      width="100%"
      itemCount={movies.length}
      itemSize={150}
    >
      {Row}
    </FixedSizeList>
  );
}

Code Splitting

Use dynamic imports to split your code into smaller chunks:

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

// Instead of: import MovieDetails from './MovieDetails';
const MovieDetails = lazy(() => import('./MovieDetails'));

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<MovieList />} />
          <Route path="/movie/:id" element={<MovieDetails />} />
        </Routes>
      </Suspense>
    </div>
  );
}

Avoid Inline Function Definitions

Inline functions create new function instances on every render:

// Bad: New function created on every render
function MovieList({ movies }) {
  return (
    <div>
      {movies.map(movie => (
        <MovieCard 
          key={movie.id} 
          {...movie} 
          onFavorite={() => handleFavorite(movie.id)} // New function every time
        />
      ))}
    </div>
  );
}

// Good: Stable function with useCallback
function MovieList({ movies }) {
  const handleFavorite = useCallback((movieId) => {
    // Handle favorite logic
  }, []);

  return (
    <div>
      {movies.map(movie => (
        <MovieCard 
          key={movie.id} 
          {...movie} 
          movieId={movie.id} // Pass ID as prop
          onFavorite={handleFavorite} // Pass stable function
        />
      ))}
    </div>
  );
}

Advanced Techniques

Lazy Loading Images

Implement lazy loading for images to improve initial load time:

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

function LazyImage({ src, alt, ...props }) {
  const [isLoaded, setIsLoaded] = useState(false);
  const imgRef = useRef();

  useEffect(() => {
    if (!imgRef.current) return;
    
    const observer = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          setIsLoaded(true);
          observer.disconnect();
        }
      });
    });
    
    observer.observe(imgRef.current);
    
    return () => {
      observer.disconnect();
    };
  }, []);

  return (
    <div ref={imgRef}>
      {isLoaded ? (
        <img src={src} alt={alt} {...props} />
      ) : (
        <div className="placeholder" />
      )}
    </div>
  );
}

Web Workers for CPU-Intensive Tasks

Move CPU-intensive operations to a web worker:

// worker.js
self.addEventListener('message', e => {
  const { data, filters } = e.data;
  
  // Expensive data processing
  const processed = data.filter(item => {
    // Complex filtering logic
    return complexFilterLogic(item, filters);
  });
  
  self.postMessage(processed);
});

// Component
function DataProcessor({ data, filters }) {
  const [processed, setProcessed] = useState([]);
  
  useEffect(() => {
    const worker = new Worker('./worker.js');
    
    worker.onmessage = e => {
      setProcessed(e.data);
    };
    
    worker.postMessage({ data, filters });
    
    return () => worker.terminate();
  }, [data, filters]);
  
  return (
    <div>
      {processed.map(item => (
        <Item key={item.id} {...item} />
      ))}
    </div>
  );
}

Conclusion

Performance optimization in React is an ongoing process. Start by measuring to identify real bottlenecks, then apply the appropriate techniques to address them. Remember that premature optimization can lead to unnecessary complexity, so focus on optimizing components that are causing actual performance issues.

By applying these techniques strategically, you can significantly improve the performance and user experience of your React applications.