React Performance Optimization

React Performance Optimization

Learn how to optimize your React applications for better performance and user experience.

Why Performance Matters

Fast applications provide better user experience, improve conversion rates, and rank higher in search results. Even small improvements in load time and responsiveness can significantly impact user satisfaction.

Measuring React Performance

React DevTools Profiler

Install React DevTools browser extension and use the Profiler tab:

  1. Open your app with React DevTools
  2. Switch to the Profiler tab
  3. Click “Record” and interact with your app
  4. Stop recording and analyze the results

Performance Monitoring

// Using React Profiler API
import { Profiler } from 'react';

function onRenderCallback(id, phase, actualDuration, baseDuration, startTime, commitTime) {
  console.log('Component render timing:', {
    id,
    phase,
    actualDuration,
    baseDuration
  });
}

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

Common Performance Issues

Unnecessary Re-renders

React re-renders components when:

  • Props change
  • State changes
  • Parent component re-renders
  • Context values change

Heavy Computations

Expensive calculations running on every render can slow down your app.

Large Bundle Sizes

Too much JavaScript code increases load times and affects performance.

Optimization Techniques

1. React.memo for Component Memoization

Prevent unnecessary re-renders of functional components:

import React from 'react';

// Without memoization - re-renders when parent re-renders
const UserCard = ({ user }) => {
  console.log('UserCard rendered');
  return (
    <div>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  );
};

// With memoization - only re-renders when props change
const UserCard = React.memo(({ user }) => {
  console.log('UserCard rendered');
  return (
    <div>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  );
});

// Custom comparison function
const UserCard = React.memo(({ user }) => {
  return (
    <div>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  );
}, (prevProps, nextProps) => {
  // Only re-render if user ID changed
  return prevProps.user.id === nextProps.user.id;
});

2. useMemo for Expensive Calculations

Cache expensive computations:

import React, { useMemo } from 'react';

function ExpensiveCalculation({ numbers }) {
  // Without useMemo - recalculates on every render
  const sum = numbers.reduce((acc, num) => acc + num, 0);
  
  // With useMemo - only recalculates when numbers change
  const sum = useMemo(() => {
    console.log('Calculating sum...');
    return numbers.reduce((acc, num) => acc + num, 0);
  }, [numbers]);

  // Complex example
  const processedData = useMemo(() => {
    return numbers
      .filter(num => num > 0)
      .map(num => num * 2)
      .sort((a, b) => a - b);
  }, [numbers]);

  return <div>Sum: {sum}</div>;
}

3. useCallback for Function References

Prevent function recreation that causes child re-renders:

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

// Without useCallback - new function on every render
const ParentComponent = () => {
  const [count, setCount] = useState(0);
  
  const handleClick = () => {
    setCount(count + 1);
  };
  
  return <ChildComponent onClick={handleClick} />;
};

// With useCallback - same function reference
const ParentComponent = () => {
  const [count, setCount] = useState(0);
  
  const handleClick = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []); // Empty dependency array
  
  // With dependencies
  const handleIncrement = useCallback((amount) => {
    setCount(prevCount => prevCount + amount);
  }, []); // Still empty since we use functional update
  
  return <ChildComponent onClick={handleClick} />;
};

const ChildComponent = React.memo(({ onClick }) => {
  console.log('ChildComponent rendered');
  return <button onClick={onClick}>Click me</button>;
});

4. Code Splitting with React.lazy

Load components only when needed:

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

// Regular import - loads immediately
import HeavyComponent from './HeavyComponent';

// Lazy loading - loads when rendered
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));

function App() {
  const [showHeavy, setShowHeavy] = useState(false);
  
  return (
    <div>
      <button onClick={() => setShowHeavy(true)}>
        Load Heavy Component
      </button>
      
      {showHeavy && (
        <Suspense fallback={<div>Loading...</div>}>
          <HeavyComponent />
        </Suspense>
      )}
    </div>
  );
}

// Route-based code splitting
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 App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/contact" element={<Contact />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

5. Virtualization for Long Lists

Use react-window or react-virtualized for large lists:

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

// Regular approach - renders all items
const RegularList = ({ items }) => (
  <div>
    {items.map(item => (
      <div key={item.id} style={{ height: 50 }}>
        {item.name}
      </div>
    ))}
  </div>
);

// Virtualized approach - renders only visible items
const VirtualizedList = ({ items }) => {
  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].name}
    </div>
  );

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

6. State Management Optimization

Use Local State When Possible

// Bad - global state for local UI
function FormComponent() {
  const [globalState, setGlobalState] useGlobalState();
  
  return (
    <form>
      <input
        value={globalState.inputValue}
        onChange={(e) => setGlobalState({ inputValue: e.target.value })}
      />
    </form>
  );
}

// Good - local state for UI concerns
function FormComponent() {
  const [inputValue, setInputValue] = useState('');
  
  const handleSubmit = () => {
    // Only update global state when needed
    updateGlobalState({ inputValue });
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
    </form>
  );
}

Context Optimization

// Bad - single context causes re-renders for all consumers
const AppContext = createContext();

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState([]);
  
  return (
    <AppContext.Provider value={{
      user, setUser, theme, setTheme, notifications, setNotifications
    }}>
      {children}
    </AppContext.Provider>
  );
}

// Good - split contexts by concern
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationContext = createContext();

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState([]);
  
  return (
    <UserContext.Provider value={{ user, setUser }}>
      <ThemeContext.Provider value={{ theme, setTheme }}>
        <NotificationContext.Provider value={{ notifications, setNotifications }}>
          {children}
        </NotificationContext.Provider>
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

7. Image Optimization

// Lazy loading images
import { useState, useRef, useEffect } from 'react';

function LazyImage({ src, alt, placeholder }) {
  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="image-container">
      {isInView && (
        <img
          src={src}
          alt={alt}
          onLoad={() => setIsLoaded(true)}
          style={{ opacity: isLoaded ? 1 : 0 }}
        />
      )}
      {!isLoaded && <div className="placeholder">{placeholder}</div>}
    </div>
  );
}

Bundle Optimization

1. Tree Shaking

Ensure your bundler can remove unused code:

// Bad - imports entire library
import * as _ from 'lodash';

// Good - imports specific functions
import { debounce, throttle } from 'lodash';

// Even better - use smaller alternatives
import { debounce } from 'lodash-es';

2. Dynamic Imports

Load libraries only when needed:

// Regular import - adds to main bundle
import moment from 'moment';

// Dynamic import - creates separate chunk
const loadMoment = async () => {
  const moment = await import('moment');
  return moment.default;
};

// Usage
const handleDateClick = async () => {
  const moment = await loadMoment();
  const formatted = moment().format('MMMM Do YYYY');
  console.log(formatted);
};

Performance Monitoring in Production

1. Web Vitals

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

2. Error Boundaries

import React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // Log to monitoring service
    console.error('Error caught by boundary:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

Performance Checklist

Component Level

  • Use React.memo for pure components
  • Wrap expensive calculations in useMemo
  • Use useCallback for stable function references
  • Avoid inline object definitions in JSX
  • Split large components into smaller ones

Application Level

  • Implement code splitting with React.lazy
  • Use virtualization for long lists
  • Optimize bundle size with tree shaking
  • Implement lazy loading for images
  • Use appropriate state management

Monitoring

  • Set up performance monitoring
  • Track Core Web Vitals
  • Implement error boundaries
  • Monitor bundle size over time
  • Test on real devices

Tools for Performance Optimization

Development Tools

  • React DevTools Profiler: Identify performance bottlenecks
  • Chrome DevTools: Analyze runtime performance
  • Bundle Analyzer: Visualize bundle composition

Build Tools

  • Webpack Bundle Analyzer: Analyze bundle size
  • Create React App: Built-in optimizations
  • Next.js: Automatic code splitting and optimization

Monitoring Services

  • Sentry: Error tracking and performance monitoring
  • LogRocket: Session replay and performance insights
  • New Relic: Application performance monitoring

Common Performance Pitfalls

1. Over-optimization

Don’t optimize prematurely. Measure first, then optimize.

2. Incorrect Dependencies

Wrong dependency arrays in useMemo/useCallback can cause bugs:

// Bad - missing dependency
const filteredData = useMemo(() => {
  return data.filter(item => item.category === selectedCategory);
}, []); // Missing selectedCategory

// Good - correct dependencies
const filteredData = useMemo(() => {
  return data.filter(item => item.category === selectedCategory);
}, [data, selectedCategory]);

3. Excessive Memoization

Not everything needs to be memoized. Use it judiciously.

Resources

Next Steps

Start by measuring your app’s current performance, identify the biggest bottlenecks, and apply the most impactful optimizations first. Remember that performance is an ongoing process, not a one-time fix.

Last updated on