Skip to main content

React Performance Optimization: Advanced Techniques

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

React applications can become slow as they grow in complexity. This guide covers advanced performance optimization techniques to keep your React apps lightning fast.

Understanding React Performance

Before optimizing, it’s crucial to understand how React works:

  • Virtual DOM: React maintains a virtual representation of the DOM
  • Reconciliation: Process of comparing virtual DOM trees to determine minimal changes
  • Re-rendering: When component state or props change, React re-renders the component tree

Performance Measurement Tools

1. React DevTools Profiler

// Wrap components you want to profile
import { Profiler } from 'react';

function onRenderCallback(
  id: string,
  phase: 'mount' | 'update',
  actualDuration: number,
  baseDuration: number,
  startTime: number,
  commitTime: number
) {
  console.log('Component:', id, 'Phase:', phase, 'Duration:', actualDuration);
}

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

2. Performance.mark API

import { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    performance.mark('MyComponent-render-start');
    
    return () => {
      performance.mark('MyComponent-render-end');
      performance.measure(
        'MyComponent-render',
        'MyComponent-render-start',
        'MyComponent-render-end'
      );
    };
  });
  
  // Component logic
}

Memoization Techniques

1. React.memo for Component Memoization

interface UserCardProps {
  user: User;
  onClick: (id: string) => void;
}

// Only re-renders if props actually change
const UserCard = React.memo<UserCardProps>(({ user, onClick }) => {
  return (
    <div onClick={() => onClick(user.id)}>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  );
});

// Custom comparison function for complex objects
const UserCardWithCustomComparison = React.memo<UserCardProps>(
  ({ user, onClick }) => {
    // Component implementation
  },
  (prevProps, nextProps) => {
    return (
      prevProps.user.id === nextProps.user.id &&
      prevProps.user.name === nextProps.user.name &&
      prevProps.onClick === nextProps.onClick
    );
  }
);

2. useMemo for Expensive Calculations

import { useMemo } from 'react';

interface DataTableProps {
  data: Item[];
  filters: FilterConfig;
  sortBy: SortConfig;
}

function DataTable({ data, filters, sortBy }: DataTableProps) {
  // Expensive operation - only recalculate when dependencies change
  const processedData = useMemo(() => {
    return data
      .filter(item => applyFilters(item, filters))
      .sort((a, b) => applySorting(a, b, sortBy))
      .slice(0, 100); // Pagination
  }, [data, filters, sortBy]);

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

3. useCallback for Function Memoization

import { useCallback, useState } from 'react';

interface TodoListProps {
  initialTodos: Todo[];
}

function TodoList({ initialTodos }: TodoListProps) {
  const [todos, setTodos] = useState(initialTodos);

  // Prevent unnecessary re-renders of child components
  const handleToggle = useCallback((id: string) => {
    setTodos(prev => prev.map(todo => 
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  }, []); // Empty dependency array - function never changes

  const handleDelete = useCallback((id: string) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  }, []);

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

Code Splitting and Lazy Loading

1. Route-based Code Splitting

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

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

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/profile" element={<Profile />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

2. Component-based Code Splitting

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

const HeavyChart = lazy(() => import('./HeavyChart'));
const DataTable = lazy(() => import('./DataTable'));

function Dashboard() {
  const [activeTab, setActiveTab] = useState<'chart' | 'table'>('chart');

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

Virtual Scrolling for Large Lists

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

interface VirtualizedListProps {
  items: Item[];
}

function VirtualizedList({ items }: VirtualizedListProps) {
  const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
    <div style={style}>
      <ItemComponent item={items[index]} />
    </div>
  );

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

State Management Optimization

1. Minimize State Updates

// ❌ Bad - Multiple state updates
function BadComponent() {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const fetchUser = async () => {
    setLoading(true);
    setError(null);
    try {
      const userData = await api.getUser();
      setUser(userData);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };
}

// βœ… Good - Single state update with useReducer
interface UserState {
  user: User | null;
  loading: boolean;
  error: string | null;
}

type UserAction = 
  | { type: 'FETCH_START' }
  | { type: 'FETCH_SUCCESS'; payload: User }
  | { type: 'FETCH_ERROR'; payload: string };

function userReducer(state: UserState, action: UserAction): UserState {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null };
    case 'FETCH_SUCCESS':
      return { user: action.payload, loading: false, error: null };
    case 'FETCH_ERROR':
      return { ...state, loading: false, error: action.payload };
    default:
      return state;
  }
}

function GoodComponent() {
  const [state, dispatch] = useReducer(userReducer, {
    user: null,
    loading: false,
    error: null
  });

  const fetchUser = async () => {
    dispatch({ type: 'FETCH_START' });
    try {
      const userData = await api.getUser();
      dispatch({ type: 'FETCH_SUCCESS', payload: userData });
    } catch (err) {
      dispatch({ type: 'FETCH_ERROR', payload: err.message });
    }
  };
}

2. Context Optimization

// ❌ Bad - Single large context
interface AppContextValue {
  user: User;
  theme: Theme;
  notifications: Notification[];
  // ... many other values
}

// βœ… Good - Split contexts by concern
interface UserContextValue {
  user: User;
  updateUser: (user: User) => void;
}

interface ThemeContextValue {
  theme: Theme;
  toggleTheme: () => void;
}

// Use multiple providers
function App() {
  return (
    <UserProvider>
      <ThemeProvider>
        <NotificationProvider>
          <MainApp />
        </NotificationProvider>
      </ThemeProvider>
    </UserProvider>
  );
}

Performance Best Practices

1. Avoid Inline Objects and Functions

// ❌ Bad - Creates new objects on every render
function BadComponent() {
  return (
    <div>
      <UserProfile 
        style={{ margin: 10, padding: 20 }} // New object every render
        onClick={() => console.log('clicked')} // New function every render
      />
    </div>
  );
}

// βœ… Good - Define outside or use useMemo/useCallback
const profileStyle = { margin: 10, padding: 20 };

function GoodComponent() {
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);

  return (
    <div>
      <UserProfile 
        style={profileStyle}
        onClick={handleClick}
      />
    </div>
  );
}

2. Use Keys Properly

// ❌ Bad - Using array index as key
function BadList({ items }: { items: Item[] }) {
  return (
    <div>
      {items.map((item, index) => (
        <ItemComponent key={index} item={item} />
      ))}
    </div>
  );
}

// βœ… Good - Using stable, unique identifier
function GoodList({ items }: { items: Item[] }) {
  return (
    <div>
      {items.map(item => (
        <ItemComponent key={item.id} item={item} />
      ))}
    </div>
  );
}

Bundle Optimization

1. Analyze Bundle Size

# Using webpack-bundle-analyzer
npm install --save-dev webpack-bundle-analyzer

# Add to package.json scripts
"analyze": "npm run build && npx webpack-bundle-analyzer build/static/js/*.js"

2. Tree Shaking

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

// βœ… Good - Import only what you need
import { debounce, throttle } from 'lodash';
// or
import debounce from 'lodash/debounce';

Conclusion

React performance optimization is an ongoing process that requires:

  1. Measurement first - Profile before optimizing
  2. Strategic memoization - Don’t memo everything, focus on expensive operations
  3. Smart code splitting - Load only what users need when they need it
  4. Efficient state management - Minimize re-renders and state updates
  5. Bundle optimization - Keep your JavaScript payloads lean

Remember: premature optimization is the root of all evil. Always measure, identify bottlenecks, then optimize strategically!