본문으로 건너뛰기
Previous
Next
React Performance Optimization: Complete Guide to Fast Us...

React Performance Optimization: Complete Guide to Fast User Interfaces

React Performance Optimization: Complete Guide to Fast User Interfaces

이 글의 핵심

Complete React performance optimization guide with real-world examples. Learn React.memo, hooks optimization, code splitting, bundle analysis, and advanced patterns for building high-performance React applications.

Understanding React Performance Fundamentals

React performance optimization requires understanding how React works under the hood: the virtual DOM diffing process, component reconciliation, and when re-renders occur. The key insight is that React’s default behavior is to re-render everything when state changes, which can become expensive as your app grows.

Modern React applications face performance challenges from multiple sources: unnecessary re-renders, expensive computations, large bundle sizes, and inefficient state management. Addressing these systematically will dramatically improve user experience.

The most effective optimization strategy combines preventing unnecessary work (memoization), splitting work efficiently (code splitting), and measuring everything (performance monitoring). Let’s explore each area with practical, production-ready examples.

Preventing Unnecessary Re-renders

React.memo for Component Optimization

React.memo prevents functional components from re-rendering when their props haven’t changed:

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

// Expensive component that should only render when props change
const ExpensiveUserCard = memo(({ user, onEdit }) => {
  console.log('ExpensiveUserCard rendering for:', user.name);
  
  // Simulate expensive computation
  const processedData = React.useMemo(() => {
    return {
      ...user,
      displayName: `${user.firstName} ${user.lastName}`.toUpperCase(),
      initials: `${user.firstName[0]}${user.lastName[0]}`,
      // Expensive calculation that should be memoized
      riskScore: calculateComplexRiskScore(user)
    };
  }, [user]);
  
  return (
    <div className="user-card">
      <div className="user-avatar">{processedData.initials}</div>
      <div className="user-info">
        <h3>{processedData.displayName}</h3>
        <p>Risk Score: {processedData.riskScore}</p>
        <button onClick={() => onEdit(user.id)}>Edit User</button>
      </div>
    </div>
  );
});

// Custom comparison for complex props
const UserList = memo(({ users, filters, onEditUser }) => {
  const filteredUsers = React.useMemo(() => {
    return users.filter(user => {
      if (filters.status && user.status !== filters.status) return false;
      if (filters.department && user.department !== filters.department) return false;
      if (filters.search) {
        const searchTerm = filters.search.toLowerCase();
        return user.firstName.toLowerCase().includes(searchTerm) ||
               user.lastName.toLowerCase().includes(searchTerm);
      }
      return true;
    });
  }, [users, filters]);
  
  return (
    <div className="user-list">
      {filteredUsers.map(user => (
        <ExpensiveUserCard
          key={user.id}
          user={user}
          onEdit={onEditUser}
        />
      ))}
    </div>
  );
}, (prevProps, nextProps) => {
  // Custom comparison logic for complex props
  return (
    prevProps.users === nextProps.users &&
    prevProps.filters === nextProps.filters &&
    prevProps.onEditUser === nextProps.onEditUser
  );
});

function calculateComplexRiskScore(user) {
  // Simulate expensive computation
  let score = 0;
  for (let i = 0; i < 1000; i++) {
    score += user.transactions?.length || 0;
    score += user.loginCount || 0;
  }
  return score % 100;
}

Hook Optimization Patterns

Strategic use of useCallback and useMemo prevents expensive recalculations:

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

const OptimizedUserDashboard = () => {
  const [users, setUsers] = useState([]);
  const [searchTerm, setSearchTerm] = useState('');
  const [sortConfig, setSortConfig] = useState({ key: 'name', direction: 'asc' });
  const [selectedUsers, setSelectedUsers] = useState(new Set());
  
  // Memoize expensive filtering and sorting
  const processedUsers = useMemo(() => {
    console.log('Recomputing processed users');
    
    let filtered = users;
    
    // Apply search filter
    if (searchTerm) {
      filtered = filtered.filter(user =>
        user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
        user.email.toLowerCase().includes(searchTerm.toLowerCase())
      );
    }
    
    // Apply sorting
    filtered = [...filtered].sort((a, b) => {
      const aValue = a[sortConfig.key];
      const bValue = b[sortConfig.key];
      
      if (aValue < bValue) return sortConfig.direction === 'asc' ? -1 : 1;
      if (aValue > bValue) return sortConfig.direction === 'asc' ? 1 : -1;
      return 0;
    });
    
    return filtered;
  }, [users, searchTerm, sortConfig]);
  
  // Memoize statistics calculation
  const statistics = useMemo(() => {
    console.log('Recomputing statistics');
    return {
      totalUsers: users.length,
      activeUsers: users.filter(u => u.isActive).length,
      selectedCount: selectedUsers.size,
      averageAge: users.reduce((sum, u) => sum + u.age, 0) / users.length || 0
    };
  }, [users, selectedUsers.size]);
  
  // Memoize event handlers to prevent child re-renders
  const handleUserSelect = useCallback((userId) => {
    setSelectedUsers(prev => {
      const newSet = new Set(prev);
      if (newSet.has(userId)) {
        newSet.delete(userId);
      } else {
        newSet.add(userId);
      }
      return newSet;
    });
  }, []);
  
  const handleSort = useCallback((key) => {
    setSortConfig(prevConfig => ({
      key,
      direction: prevConfig.key === key && prevConfig.direction === 'asc' ? 'desc' : 'asc'
    }));
  }, []);
  
  const handleBulkAction = useCallback((action) => {
    console.log(`Performing ${action} on ${selectedUsers.size} users`);
    // Implement bulk actions
    setSelectedUsers(new Set());
  }, [selectedUsers.size]);
  
  // Debounced search to prevent excessive filtering
  const debouncedSearch = useCallback(
    debounce((term) => setSearchTerm(term), 300),
    []
  );
  
  return (
    <div className="user-dashboard">
      <DashboardHeader 
        statistics={statistics}
        onBulkAction={handleBulkAction}
        hasSelection={selectedUsers.size > 0}
      />
      
      <SearchAndFilter
        onSearch={debouncedSearch}
        onSort={handleSort}
        sortConfig={sortConfig}
      />
      
      <UserList
        users={processedUsers}
        selectedUsers={selectedUsers}
        onUserSelect={handleUserSelect}
      />
    </div>
  );
};

// Utility function for debouncing
function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

Advanced Memoization Strategies

Context Optimization

Prevent context re-renders from cascading through your component tree:

import React, { createContext, useContext, useMemo, useState } from 'react';

// Split contexts by update frequency
const UserDataContext = createContext();
const UserActionsContext = createContext();

const UserProvider = ({ children }) => {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  // Memoize actions to prevent unnecessary re-renders
  const actions = useMemo(() => ({
    addUser: async (userData) => {
      setLoading(true);
      try {
        const response = await fetch('/api/users', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(userData)
        });
        const newUser = await response.json();
        setUsers(prev => [...prev, newUser]);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    },
    
    updateUser: async (userId, updates) => {
      setUsers(prev => prev.map(user => 
        user.id === userId ? { ...user, ...updates } : user
      ));
    },
    
    deleteUser: async (userId) => {
      setUsers(prev => prev.filter(user => user.id !== userId));
    }
  }), []); // Actions don't depend on state, so empty deps
  
  // Memoize data to prevent re-renders when actions haven't changed
  const userData = useMemo(() => ({
    users,
    loading,
    error
  }), [users, loading, error]);
  
  return (
    <UserActionsContext.Provider value={actions}>
      <UserDataContext.Provider value={userData}>
        {children}
      </UserDataContext.Provider>
    </UserActionsContext.Provider>
  );
};

// Custom hooks for consuming context
const useUserData = () => {
  const context = useContext(UserDataContext);
  if (!context) {
    throw new Error('useUserData must be used within UserProvider');
  }
  return context;
};

const useUserActions = () => {
  const context = useContext(UserActionsContext);
  if (!context) {
    throw new Error('useUserActions must be used within UserProvider');
  }
  return context;
};

// Component that only needs actions (won't re-render when data changes)
const AddUserForm = memo(() => {
  const { addUser } = useUserActions();
  const [formData, setFormData] = useState({ name: '', email: '' });
  
  const handleSubmit = useCallback(async (e) => {
    e.preventDefault();
    await addUser(formData);
    setFormData({ name: '', email: '' });
  }, [addUser, formData]);
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        value={formData.name}
        onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
        placeholder="Name"
      />
      <input 
        value={formData.email}
        onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
        placeholder="Email"
      />
      <button type="submit">Add User</button>
    </form>
  );
});

// Component that only needs data (won't re-render when actions change)
const UserStats = memo(() => {
  const { users } = useUserData();
  
  const stats = useMemo(() => ({
    total: users.length,
    active: users.filter(u => u.isActive).length,
    premium: users.filter(u => u.isPremium).length
  }), [users]);
  
  return (
    <div className="user-stats">
      <div>Total: {stats.total}</div>
      <div>Active: {stats.active}</div>
      <div>Premium: {stats.premium}</div>
    </div>
  );
});

Virtualization for Large Lists

Handle thousands of items efficiently with windowing:

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

const VirtualizedUserList = memo(({ users, onUserClick, searchTerm }) => {
  // Filter users based on search
  const filteredUsers = useMemo(() => {
    if (!searchTerm) return users;
    return users.filter(user =>
      user.name.toLowerCase().includes(searchTerm.toLowerCase())
    );
  }, [users, searchTerm]);
  
  // Row renderer for react-window
  const Row = memo(({ index, style }) => {
    const user = filteredUsers[index];
    
    return (
      <div style={style} className="user-row">
        <UserRowContent user={user} onUserClick={onUserClick} />
      </div>
    );
  });
  
  // Calculate item size dynamically if needed
  const getItemSize = useCallback((index) => {
    // Return dynamic height based on content
    const user = filteredUsers[index];
    return user.isExpanded ? 120 : 60;
  }, [filteredUsers]);
  
  return (
    <List
      height={600}
      itemCount={filteredUsers.length}
      itemSize={60}
      itemData={filteredUsers}
      overscanCount={5} // Render extra items for smooth scrolling
    >
      {Row}
    </List>
  );
});

const UserRowContent = memo(({ user, onUserClick }) => {
  const handleClick = useCallback(() => {
    onUserClick(user.id);
  }, [user.id, onUserClick]);
  
  return (
    <div className="user-content" onClick={handleClick}>
      <img src={user.avatar} alt={user.name} className="user-avatar" />
      <div className="user-info">
        <h4>{user.name}</h4>
        <p>{user.email}</p>
      </div>
      <div className="user-status">
        {user.isActive ? '🟢' : '🔴'}
      </div>
    </div>
  );
});

Code Splitting and Lazy Loading

Route-Based Code Splitting

Split your application by routes to reduce initial bundle size:

import React, { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import LoadingSpinner from './components/LoadingSpinner';
import ErrorBoundary from './components/ErrorBoundary';

// Lazy load route components
const Dashboard = lazy(() => import('./pages/Dashboard'));
const UserManagement = lazy(() => import('./pages/UserManagement'));
const Analytics = lazy(() => import('./pages/Analytics'));
const Settings = lazy(() => import('./pages/Settings'));

// Preload critical routes
const preloadUserManagement = () => import('./pages/UserManagement');
const preloadAnalytics = () => import('./pages/Analytics');

const App = () => {
  React.useEffect(() => {
    // Preload likely next pages after initial load
    const timer = setTimeout(() => {
      preloadUserManagement();
      preloadAnalytics();
    }, 2000);
    
    return () => clearTimeout(timer);
  }, []);
  
  return (
    <BrowserRouter>
      <div className="app">
        <Navigation 
          onUserManagementHover={preloadUserManagement}
          onAnalyticsHover={preloadAnalytics}
        />
        
        <main className="main-content">
          <ErrorBoundary>
            <Suspense fallback={<LoadingSpinner />}>
              <Routes>
                <Route path="/" element={<Dashboard />} />
                <Route path="/users" element={<UserManagement />} />
                <Route path="/analytics" element={<Analytics />} />
                <Route path="/settings" element={<Settings />} />
              </Routes>
            </Suspense>
          </ErrorBoundary>
        </main>
      </div>
    </BrowserRouter>
  );
};

// Navigation with hover preloading
const Navigation = memo(({ onUserManagementHover, onAnalyticsHover }) => {
  return (
    <nav className="navigation">
      <Link to="/">Dashboard</Link>
      <Link 
        to="/users" 
        onMouseEnter={onUserManagementHover}
      >
        Users
      </Link>
      <Link 
        to="/analytics"
        onMouseEnter={onAnalyticsHover}
      >
        Analytics
      </Link>
      <Link to="/settings">Settings</Link>
    </nav>
  );
});

Component-Level Code Splitting

Split large components or feature sets dynamically:

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

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

// Preload function for better UX
const preloadChart = () => import('./ChartComponent');

const AnalyticsDashboard = () => {
  const [activeTab, setActiveTab] = useState('overview');
  const [chartLoaded, setChartLoaded] = useState(false);
  
  const handleTabChange = (tab) => {
    setActiveTab(tab);
    
    // Preload chart when user shows interest
    if (tab === 'charts' && !chartLoaded) {
      preloadChart().then(() => setChartLoaded(true));
    }
  };
  
  return (
    <div className="analytics-dashboard">
      <TabNavigation 
        activeTab={activeTab} 
        onTabChange={handleTabChange}
        onChartsHover={preloadChart}
      />
      
      <div className="tab-content">
        {activeTab === 'overview' && (
          <OverviewTab />
        )}
        
        {activeTab === 'charts' && (
          <Suspense fallback={<ChartSkeleton />}>
            <ChartComponent />
          </Suspense>
        )}
        
        {activeTab === 'data' && (
          <Suspense fallback={<TableSkeleton />}>
            <DataTable />
          </Suspense>
        )}
        
        {activeTab === 'reports' && (
          <Suspense fallback={<ReportSkeleton />}>
            <ReportGenerator />
          </Suspense>
        )}
      </div>
    </div>
  );
};

// Skeleton components for better perceived performance
const ChartSkeleton = () => (
  <div className="chart-skeleton">
    <div className="skeleton-header"></div>
    <div className="skeleton-chart"></div>
  </div>
);

Bundle Optimization and Analysis

Webpack Bundle Analysis

Understand and optimize your bundle composition:

// webpack.config.js optimizations
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        // Separate vendor libraries
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10,
          chunks: 'all',
        },
        
        // Common chunks across multiple entries
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true,
        },
        
        // Large libraries get their own chunks
        lodash: {
          test: /[\\/]node_modules[\\/](lodash)[\\/]/,
          name: 'lodash',
          priority: 20,
          chunks: 'all',
        },
        
        moment: {
          test: /[\\/]node_modules[\\/](moment)[\\/]/,
          name: 'moment',
          priority: 20,
          chunks: 'all',
        },
      },
    },
    
    // Generate runtime chunk
    runtimeChunk: {
      name: 'runtime',
    },
  },
  
  // Tree shaking optimization
  resolve: {
    alias: {
      // Use ES modules version of lodash for better tree shaking
      'lodash': 'lodash-es',
    },
  },
};

// Package.json script for bundle analysis
{
  "scripts": {
    "analyze": "npm run build && npx webpack-bundle-analyzer build/static/js/*.js"
  }
}

Tree Shaking Optimization

Ensure unused code is eliminated:

// utils/index.js - Poor tree shaking (imports everything)
export * from './dateUtils';
export * from './stringUtils';
export * from './arrayUtils';

// utils/index.js - Better tree shaking (explicit exports)
export { formatDate, parseDate } from './dateUtils';
export { capitalize, truncate } from './stringUtils';
export { chunk, unique } from './arrayUtils';

// Component usage - Import only what you need
import { formatDate, capitalize } from './utils';

// Avoid importing entire libraries
// ❌ Bad - imports entire lodash
import _ from 'lodash';

// ✅ Good - imports only needed functions
import { debounce, throttle } from 'lodash-es';

// ✅ Even better - individual imports
import debounce from 'lodash-es/debounce';
import throttle from 'lodash-es/throttle';

Performance Monitoring and Debugging

React DevTools Integration

Implement comprehensive performance monitoring:

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

// Performance monitoring wrapper
const PerformanceWrapper = ({ children, id }) => {
  const [measurements, setMeasurements] = useState([]);
  
  const onRenderCallback = (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
    // Log performance data
    const measurement = {
      id,
      phase,
      actualDuration,
      baseDuration,
      startTime,
      commitTime,
      timestamp: Date.now()
    };
    
    setMeasurements(prev => [...prev.slice(-50), measurement]); // Keep last 50 measurements
    
    // Alert on performance issues
    if (actualDuration > 16) { // More than one frame
      console.warn(`Slow render in ${id}: ${actualDuration.toFixed(2)}ms`);
    }
  };
  
  return (
    <Profiler id={id} onRender={onRenderCallback}>
      {children}
    </Profiler>
  );
};

// Custom hook for performance tracking
const usePerformanceTracker = (componentName) => {
  const [renderCount, setRenderCount] = useState(0);
  
  React.useEffect(() => {
    setRenderCount(prev => prev + 1);
  });
  
  React.useEffect(() => {
    if (renderCount > 0) {
      console.log(`${componentName} rendered ${renderCount} times`);
    }
  }, [renderCount, componentName]);
  
  return { renderCount };
};

// Usage in components
const MonitoredComponent = () => {
  const { renderCount } = usePerformanceTracker('MonitoredComponent');
  
  return (
    <PerformanceWrapper id="monitored-component">
      <div>Render count: {renderCount}</div>
      {/* Component content */}
    </PerformanceWrapper>
  );
};

Custom Performance Hooks

Build reusable performance utilities:

// hooks/useRenderOptimization.js
import { useRef, useCallback, useMemo } from 'react';

export const useStableCallback = (callback, deps) => {
  const callbackRef = useRef(callback);
  const depsRef = useRef(deps);
  
  // Update callback if dependencies changed
  if (!deps || deps.some((dep, i) => dep !== depsRef.current?.[i])) {
    callbackRef.current = callback;
    depsRef.current = deps;
  }
  
  return useCallback((...args) => {
    return callbackRef.current(...args);
  }, []);
};

export const useDeepMemo = (factory, deps) => {
  const depsRef = useRef();
  const resultRef = useRef();
  
  if (!depsRef.current || !deepEqual(deps, depsRef.current)) {
    depsRef.current = deps;
    resultRef.current = factory();
  }
  
  return resultRef.current;
};

// Deep equality check (simplified)
function deepEqual(a, b) {
  if (a === b) return true;
  if (!a || !b) return false;
  if (typeof a !== 'object' || typeof b !== 'object') return false;
  
  const keysA = Object.keys(a);
  const keysB = Object.keys(b);
  
  if (keysA.length !== keysB.length) return false;
  
  for (let key of keysA) {
    if (!keysB.includes(key) || !deepEqual(a[key], b[key])) {
      return false;
    }
  }
  
  return true;
}

// Usage example
const OptimizedComponent = ({ complexData, onUpdate }) => {
  // Stable callback that doesn't cause re-renders
  const handleUpdate = useStableCallback((id, changes) => {
    onUpdate(id, changes);
  }, [onUpdate]);
  
  // Deep memoization for complex objects
  const processedData = useDeepMemo(() => {
    return complexData.map(item => ({
      ...item,
      computed: expensiveComputation(item)
    }));
  }, [complexData]);
  
  return (
    <div>
      {processedData.map(item => (
        <ItemComponent 
          key={item.id} 
          item={item} 
          onUpdate={handleUpdate}
        />
      ))}
    </div>
  );
};

React performance optimization is about making deliberate choices based on your application’s specific needs. Start with measuring performance, identify bottlenecks, then apply targeted optimizations. The goal isn’t to optimize everything, but to optimize the right things at the right time.

Focus on user-perceived performance: fast initial loads, smooth interactions, and responsive interfaces. Use the tools and patterns shown here to build React applications that scale efficiently and provide excellent user experiences regardless of complexity.