Performance

React Performance Optimization: From Basics to Advanced Techniques

Learn proven techniques to optimize React application performance, from simple optimizations to advanced patterns that can dramatically improve your app's speed.

By Karthik Srinivas
#react #performance #optimization #javascript #web development

Introduction

React applications can become slow if not properly optimized. Understanding React’s rendering behavior and implementing the right optimization techniques can dramatically improve your app’s performance. This guide covers everything from basic optimizations to advanced patterns.

Understanding React Rendering

Before diving into optimization techniques, it’s crucial to understand how React renders components:

  1. Initial Render: React creates a virtual DOM representation
  2. Re-render: React compares the new virtual DOM with the previous one
  3. Reconciliation: React updates only the parts of the real DOM that changed

When Does React Re-render?

React re-renders a component when:

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

Basic Optimization Techniques

1. Use React.memo for Functional Components

import React from 'react';

const ExpensiveComponent = React.memo(({ data, onAction }) => {
  console.log('Rendering ExpensiveComponent');
  
  return (
    <div>
      <h2>{data.title}</h2>
      <p>{data.description}</p>
      <button onClick={onAction}>Action</button>
    </div>
  );
});

// With custom comparison
const OptimizedComponent = React.memo(({ user, settings }) => {
  return <div>{user.name} - {settings.theme}</div>;
}, (prevProps, nextProps) => {
  return prevProps.user.id === nextProps.user.id &&
         prevProps.settings.theme === nextProps.settings.theme;
});

2. Use useMemo for Expensive Calculations

import React, { useMemo } from 'react';

function DataVisualization({ data, filters }) {
  const processedData = useMemo(() => {
    console.log('Processing data...');
    return data
      .filter(item => filters.includes(item.category))
      .map(item => ({
        ...item,
        calculated: expensiveCalculation(item.value)
      }));
  }, [data, filters]);

  return (
    <div>
      {processedData.map(item => (
        <div key={item.id}>{item.calculated}</div>
      ))}
    </div>
  );
}

3. Use useCallback for Function Stability

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

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

  const handleToggle = useCallback((id) => {
    // Toggle logic here
    console.log('Toggling todo:', id);
  }, []);

  const handleFilter = useCallback((newFilter) => {
    setFilter(newFilter);
  }, []);

  return (
    <div>
      <FilterButtons onFilter={handleFilter} />
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={handleToggle}
        />
      ))}
    </div>
  );
}

Advanced Optimization Patterns

1. Component Splitting and Lazy Loading

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

// Lazy load heavy components
const HeavyChart = lazy(() => import('./HeavyChart'));
const DataTable = lazy(() => import('./DataTable'));

function Dashboard({ activeTab, data }) {
  return (
    <div>
      <nav>
        <button onClick={() => setActiveTab('chart')}>Chart</button>
        <button onClick={() => setActiveTab('table')}>Table</button>
      </nav>
      
      <Suspense fallback={<div>Loading...</div>}>
        {activeTab === 'chart' && <HeavyChart data={data} />}
        {activeTab === 'table' && <DataTable data={data} />}
      </Suspense>
    </div>
  );
}

2. Virtualization for Large Lists

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

function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      <div>Item {index}: {items[index].name}</div>
    </div>
  );

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

3. State Management Optimization

import React, { createContext, useContext, useReducer } from 'react';

// Split contexts to minimize re-renders
const UserContext = createContext();
const SettingsContext = createContext();

function UserProvider({ children }) {
  const [user, dispatch] = useReducer(userReducer, initialUser);
  
  return (
    <UserContext.Provider value={{ user, dispatch }}>
      {children}
    </UserContext.Provider>
  );
}

function SettingsProvider({ children }) {
  const [settings, setSettings] = useState(initialSettings);
  
  return (
    <SettingsContext.Provider value={{ settings, setSettings }}>
      {children}
    </SettingsContext.Provider>
  );
}

// Custom hooks for context consumption
function useUser() {
  const context = useContext(UserContext);
  if (!context) {
    throw new Error('useUser must be used within UserProvider');
  }
  return context;
}

Performance Monitoring

1. React DevTools Profiler

import React, { Profiler } from 'react';

function onRenderCallback(id, phase, actualDuration) {
  console.log('Component:', id);
  console.log('Phase:', phase);
  console.log('Duration:', actualDuration);
}

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

2. Custom Performance Hook

import { useEffect, useRef } from 'react';

function usePerformanceMonitor(componentName) {
  const renderCount = useRef(0);
  const startTime = useRef(performance.now());

  useEffect(() => {
    renderCount.current += 1;
    const endTime = performance.now();
    const duration = endTime - startTime.current;
    
    console.log(`${componentName} rendered ${renderCount.current} times`);
    console.log(`Last render took ${duration}ms`);
    
    startTime.current = performance.now();
  });
}

function MyComponent() {
  usePerformanceMonitor('MyComponent');
  // Component logic
}

Bundle Size Optimization

1. Code Splitting

import React, { Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

// Route-based code splitting
const Home = React.lazy(() => import('./pages/Home'));
const About = React.lazy(() => import('./pages/About'));
const Contact = React.lazy(() => import('./pages/Contact'));

function App() {
  return (
    <Router>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/contact" element={<Contact />} />
        </Routes>
      </Suspense>
    </Router>
  );
}

2. Tree Shaking

// Instead of importing the entire library
import * as _ from 'lodash';

// Import only what you need
import debounce from 'lodash/debounce';
import throttle from 'lodash/throttle';

// Or use ES6 modules
import { debounce, throttle } from 'lodash-es';

Common Anti-Patterns to Avoid

1. Creating Objects/Arrays in Render

// ❌ Bad - Creates new object on every render
function BadComponent({ items }) {
  return (
    <ChildComponent 
      style={{ margin: '10px' }}
      items={items.filter(item => item.active)}
    />
  );
}

// ✅ Good - Memoize expensive operations
function GoodComponent({ items }) {
  const activeItems = useMemo(() => 
    items.filter(item => item.active), [items]
  );
  
  const styles = useMemo(() => ({ margin: '10px' }), []);
  
  return (
    <ChildComponent 
      style={styles}
      items={activeItems}
    />
  );
}

2. Unnecessary Re-renders

// ❌ Bad - Parent re-renders cause child re-renders
function Parent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Count: {count}
      </button>
      <ExpensiveChild name={name} />
    </div>
  );
}

// ✅ Good - Memoize child component
const ExpensiveChild = React.memo(({ name }) => {
  // Expensive rendering logic
  return <div>{name}</div>;
});

Performance Testing

1. Measuring Performance

import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';

function measurePerformance() {
  getCLS(console.log);
  getFID(console.log);
  getFCP(console.log);
  getLCP(console.log);
  getTTFB(console.log);
}

// Call this in your app
measurePerformance();

2. Performance Budget

// webpack.config.js
module.exports = {
  performance: {
    maxAssetSize: 250000,
    maxEntrypointSize: 250000,
    hints: 'warning'
  }
};

Conclusion

React performance optimization is an ongoing process that requires understanding your app’s specific bottlenecks. Start with profiling to identify issues, then apply the appropriate optimization techniques. Remember:

  1. Measure first - Use React DevTools Profiler
  2. Optimize strategically - Focus on components that re-render frequently
  3. Test the impact - Verify that optimizations actually improve performance
  4. Don’t over-optimize - Some optimizations can make code harder to maintain

The key is finding the right balance between performance and maintainability. Focus on the optimizations that provide the most impact for your specific use case.

Happy optimizing! 🚀