DEVELOPMENT

React performance optimization: Complete guide to Memo, useCallback, and advanced techniques

Master React performance optimization with comprehensive guide covering React.memo, useMemo, useCallback, lazy loading, and advanced concurrent features for production applications.

Vladimir Siedykh

How Performance Debt Compounds Into User Experience Problems

Your React app loads in 8 seconds on a mobile device. Users are bouncing before they see your content. The bundle size has grown to 2.5MB, and your components re-render dozens of times for a single user interaction. Sound familiar?

Performance problems in React applications don't happen overnight. They accumulate gradually - an unnecessary re-render here, an unoptimized image there, a heavy library bundled everywhere. By the time you notice the sluggish behavior, the performance debt has compounded into a user experience problem that's driving away customers.

The challenge isn't just identifying performance bottlenecks; it's knowing which optimizations actually matter. I've seen developers spend hours memoizing every component only to discover that their real performance issue was an unoptimized database query or a massive image loading on every page.

This guide focuses on the performance optimizations that make a measurable difference in real applications. You'll learn how to identify actual bottlenecks, apply the right optimization techniques, and build React applications that remain fast as they scale. We'll explore everything from preventing unnecessary re-renders to implementing advanced code splitting strategies.

By the end, you'll have a systematic approach to React performance that prioritizes user experience and provides measurable improvements rather than premature optimization.

Understanding React performance fundamentals

Before diving into specific optimization techniques, it's crucial to understand how React's rendering process works and where performance bottlenecks typically occur.

React's rendering cycle explained

React's performance characteristics stem from its virtual DOM reconciliation process. Every state change triggers a series of steps that determine what gets updated in the browser:

// Understanding the render cycle
function PerformanceExample() {
  const [count, setCount] = useState(0);
  const [users, setUsers] = useState([]);
  
  // This runs on every render - potential performance issue
  const expensiveCalculation = () => {
    console.log('Running expensive calculation...');
    return count * 1000 + Math.random();
  };
  
  // Every time count or users change, this component re-renders
  // and expensiveCalculation runs again
  const result = expensiveCalculation();
  
  return (
    <div>
      <p>Count: {count}</p>
      <p>Result: {result}</p>
      <p>Users: {users.length}</p>
      <button onClick={() => setCount(c => c + 1)}>
        Increment Count
      </button>
      <button onClick={() => setUsers([...users, { id: Date.now() }])}>
        Add User
      </button>
    </div>
  );
}

The key insight is that React re-renders components when their state or props change, but it doesn't automatically optimize expensive operations within those renders.

Measuring performance effectively

Before optimizing, you need baseline measurements. React DevTools Profiler provides detailed insights into your application's rendering behavior:

import { Profiler } from 'react';

function onRenderCallback(id, phase, actualDuration, baseDuration, startTime, commitTime) {
  // Log performance data
  console.log('Component:', id);
  console.log('Phase:', phase); // 'mount' or 'update'
  console.log('Actual render time:', actualDuration);
  console.log('Base render time (without memoization):', baseDuration);
  
  // Send to analytics in production
  if (actualDuration > 16) { // Slower than 60fps
    analytics.track('slow_render', {
      componentId: id,
      renderTime: actualDuration,
      phase
    });
  }
}

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

Performance budgets and metrics

Establish performance budgets for different aspects of your application:

// Performance monitoring hook
function usePerformanceBudget() {
  const [metrics, setMetrics] = useState({
    renderTime: 0,
    bundleSize: 0,
    imageSize: 0,
    apiResponseTime: 0
  });
  
  const budgets = {
    renderTime: 16, // 60fps threshold
    bundleSize: 250, // 250KB initial bundle
    imageSize: 100, // 100KB per image
    apiResponseTime: 1000 // 1 second API calls
  };
  
  const checkBudget = useCallback((metric, value) => {
    const budget = budgets[metric];
    const exceeded = value > budget;
    
    if (exceeded) {
      console.warn(`Performance budget exceeded for ${metric}:`, {
        value,
        budget,
        overage: value - budget
      });
    }
    
    setMetrics(prev => ({ ...prev, [metric]: value }));
    return !exceeded;
  }, []);
  
  return { metrics, budgets, checkBudget };
}

Preventing unnecessary re-renders

The most common React performance issue is components re-rendering when they don't need to. Building on the patterns from our React hooks guide, let's explore targeted solutions.

React.memo for component memoization

React.memo prevents re-renders when props haven't changed:

// Without memoization - re-renders every time parent renders
function ExpensiveChildComponent({ data, onUpdate }) {
  console.log('Child component rendered');
  
  return (
    <div>
      {data.map(item => (
        <div key={item.id}>
          <h3>{item.title}</h3>
          <p>{item.description}</p>
          <button onClick={() => onUpdate(item.id)}>Update</button>
        </div>
      ))}
    </div>
  );
}

// With memoization - only re-renders when data or onUpdate changes
const MemoizedChildComponent = React.memo(function ExpensiveChildComponent({ data, onUpdate }) {
  console.log('Child component rendered');
  
  return (
    <div>
      {data.map(item => (
        <div key={item.id}>
          <h3>{item.title}</h3>
          <p>{item.description}</p>
          <button onClick={() => onUpdate(item.id)}>Update</button>
        </div>
      ))}
    </div>
  );
});

// Parent component with proper optimization
function ParentComponent() {
  const [data, setData] = useState([]);
  const [otherState, setOtherState] = useState(0);
  
  // useCallback prevents onUpdate from changing on every render
  const handleUpdate = useCallback((id) => {
    setData(prevData => 
      prevData.map(item => 
        item.id === id 
          ? { ...item, updated: true }
          : item
      )
    );
  }, []);
  
  return (
    <div>
      <button onClick={() => setOtherState(s => s + 1)}>
        Other State: {otherState}
      </button>
      
      {/* This won't re-render when otherState changes */}
      <MemoizedChildComponent 
        data={data} 
        onUpdate={handleUpdate} 
      />
    </div>
  );
}

Custom comparison functions for complex props

When simple shallow comparison isn't sufficient, provide custom comparison logic:

// Complex props that need custom comparison
interface ProductCardProps {
  product: {
    id: number;
    name: string;
    price: number;
    metadata: {
      tags: string[];
      reviews: Review[];
      images: string[];
    };
  };
  displayMode: 'compact' | 'detailed';
  onAddToCart: (productId: number) => void;
}

function ProductCard({ product, displayMode, onAddToCart }: ProductCardProps) {
  return (
    <div className={`product-card ${displayMode}`}>
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      {displayMode === 'detailed' && (
        <div>
          <p>Tags: {product.metadata.tags.join(', ')}</p>
          <p>Reviews: {product.metadata.reviews.length}</p>
        </div>
      )}
      <button onClick={() => onAddToCart(product.id)}>
        Add to Cart
      </button>
    </div>
  );
}

// Custom comparison function
const MemoizedProductCard = React.memo(ProductCard, (prevProps, nextProps) => {
  // Only re-render if these specific fields change
  const prevProduct = prevProps.product;
  const nextProduct = nextProps.product;
  
  return (
    prevProduct.id === nextProduct.id &&
    prevProduct.name === nextProduct.name &&
    prevProduct.price === nextProduct.price &&
    prevProps.displayMode === nextProps.displayMode &&
    // For metadata, only check what we actually render
    (prevProps.displayMode === 'compact' || (
      prevProduct.metadata.tags.length === nextProduct.metadata.tags.length &&
      prevProduct.metadata.reviews.length === nextProduct.metadata.reviews.length
    ))
  );
});

Optimizing context usage

Context can cause performance issues when overused. Split contexts and optimize providers:

// ❌ Single large context causes unnecessary re-renders
const AppContext = createContext({
  user: null,
  theme: 'light',
  notifications: [],
  cart: [],
  preferences: {}
});

// ✅ Split contexts by update frequency
const UserContext = createContext(null);
const ThemeContext = createContext('light');
const CartContext = createContext([]);

// Optimized context provider with memoization
function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  
  // Memoize context value to prevent unnecessary re-renders
  const value = useMemo(() => ({
    user,
    setUser,
    isAuthenticated: !!user,
    hasRole: (role) => user?.roles?.includes(role) || false
  }), [user]);
  
  return (
    <UserContext.Provider value={value}>
      {children}
    </UserContext.Provider>
  );
}

// Selector-based context for complex state
function createSelectableContext(initialState) {
  const StateContext = createContext(initialState);
  const UpdateContext = createContext(() => {});
  
  function Provider({ children }) {
    const [state, setState] = useState(initialState);
    
    return (
      <StateContext.Provider value={state}>
        <UpdateContext.Provider value={setState}>
          {children}
        </UpdateContext.Provider>
      </StateContext.Provider>
    );
  }
  
  function useSelector(selector) {
    const state = useContext(StateContext);
    const selectedState = useMemo(() => selector(state), [state, selector]);
    return selectedState;
  }
  
  function useUpdate() {
    return useContext(UpdateContext);
  }
  
  return { Provider, useSelector, useUpdate };
}

// Usage with selective subscriptions
const { Provider: StoreProvider, useSelector, useUpdate } = createSelectableContext({
  user: null,
  cart: [],
  theme: 'light'
});

function CartComponent() {
  // Only re-renders when cart changes, not user or theme
  const cart = useSelector(state => state.cart);
  const updateStore = useUpdate();
  
  const addToCart = (item) => {
    updateStore(prev => ({
      ...prev,
      cart: [...prev.cart, item]
    }));
  };
  
  return <div>Cart has {cart.length} items</div>;
}

Advanced memoization techniques

Beyond basic React.memo, sophisticated memoization patterns help optimize complex scenarios.

useMemo for expensive calculations

Use useMemo to cache expensive computations:

function DataVisualization({ rawData, filters, sortBy }) {
  // Expensive data processing that should only run when dependencies change
  const processedData = useMemo(() => {
    console.log('Processing data...');
    
    return rawData
      .filter(item => {
        return filters.every(filter => {
          switch (filter.type) {
            case 'range':
              return item[filter.field] >= filter.min && item[filter.field] <= filter.max;
            case 'text':
              return item[filter.field].toLowerCase().includes(filter.value.toLowerCase());
            case 'category':
              return filter.values.includes(item[filter.field]);
            default:
              return true;
          }
        });
      })
      .sort((a, b) => {
        const direction = sortBy.direction === 'asc' ? 1 : -1;
        return (a[sortBy.field] - b[sortBy.field]) * direction;
      })
      .map(item => ({
        ...item,
        calculatedMetrics: {
          trend: calculateTrend(item.values),
          average: calculateAverage(item.values),
          variance: calculateVariance(item.values)
        }
      }));
  }, [rawData, filters, sortBy]);
  
  // Expensive chart configuration that depends on processed data
  const chartConfig = useMemo(() => {
    console.log('Generating chart config...');
    
    return {
      type: 'line',
      data: {
        labels: processedData.map(item => item.label),
        datasets: [{
          label: 'Values',
          data: processedData.map(item => item.value),
          borderColor: 'rgb(75, 192, 192)',
          tension: 0.1
        }]
      },
      options: {
        responsive: true,
        scales: {
          y: {
            min: Math.min(...processedData.map(item => item.value)),
            max: Math.max(...processedData.map(item => item.value))
          }
        }
      }
    };
  }, [processedData]);
  
  return (
    <div>
      <div className="stats">
        <p>Total items: {processedData.length}</p>
        <p>Average: {processedData.reduce((sum, item) => sum + item.value, 0) / processedData.length}</p>
      </div>
      <Chart config={chartConfig} />
    </div>
  );
}

useCallback for stable function references

Prevent child re-renders by stabilizing function props:

function TodoList({ todos, onToggle, onDelete, onEdit }) {
  const [filter, setFilter] = useState('all');
  const [editingId, setEditingId] = useState(null);
  
  // Filtered todos with memoization
  const filteredTodos = useMemo(() => {
    switch (filter) {
      case 'active':
        return todos.filter(todo => !todo.completed);
      case 'completed':
        return todos.filter(todo => todo.completed);
      default:
        return todos;
    }
  }, [todos, filter]);
  
  // Memoized handlers to prevent child re-renders
  const handleToggle = useCallback((id) => {
    onToggle(id);
  }, [onToggle]);
  
  const handleDelete = useCallback((id) => {
    onDelete(id);
    if (editingId === id) {
      setEditingId(null);
    }
  }, [onDelete, editingId]);
  
  const handleEdit = useCallback((id, newText) => {
    onEdit(id, newText);
    setEditingId(null);
  }, [onEdit]);
  
  const startEditing = useCallback((id) => {
    setEditingId(id);
  }, []);
  
  const cancelEditing = useCallback(() => {
    setEditingId(null);
  }, []);
  
  return (
    <div className="todo-list">
      <FilterButtons 
        currentFilter={filter} 
        onFilterChange={setFilter} 
      />
      
      <div className="todos">
        {filteredTodos.map(todo => (
          <TodoItem
            key={todo.id}
            todo={todo}
            isEditing={editingId === todo.id}
            onToggle={handleToggle}
            onDelete={handleDelete}
            onEdit={handleEdit}
            onStartEditing={startEditing}
            onCancelEditing={cancelEditing}
          />
        ))}
      </div>
    </div>
  );
}

// Memoized child component
const TodoItem = React.memo(function TodoItem({
  todo,
  isEditing,
  onToggle,
  onDelete,
  onEdit,
  onStartEditing,
  onCancelEditing
}) {
  const [editText, setEditText] = useState(todo.text);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    if (editText.trim()) {
      onEdit(todo.id, editText.trim());
    }
  };
  
  if (isEditing) {
    return (
      <form onSubmit={handleSubmit} className="todo-item editing">
        <input
          type="text"
          value={editText}
          onChange={(e) => setEditText(e.target.value)}
          autoFocus
        />
        <button type="submit">Save</button>
        <button type="button" onClick={onCancelEditing}>Cancel</button>
      </form>
    );
  }
  
  return (
    <div className={`todo-item ${todo.completed ? 'completed' : ''}`}>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span 
        className="todo-text"
        onDoubleClick={() => onStartEditing(todo.id)}
      >
        {todo.text}
      </span>
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </div>
  );
});

Custom memoization hooks

Create reusable memoization patterns for common scenarios:

// Custom hook for debounced values
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    
    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);
  
  return debouncedValue;
}

// Custom hook for memoized async operations
function useMemoizedAsync(asyncFn, deps) {
  const [state, setState] = useState({
    data: null,
    loading: true,
    error: null
  });
  
  const memoizedFn = useCallback(asyncFn, deps);
  
  useEffect(() => {
    let cancelled = false;
    
    setState({ data: null, loading: true, error: null });
    
    memoizedFn()
      .then(data => {
        if (!cancelled) {
          setState({ data, loading: false, error: null });
        }
      })
      .catch(error => {
        if (!cancelled) {
          setState({ data: null, loading: false, error });
        }
      });
    
    return () => {
      cancelled = true;
    };
  }, [memoizedFn]);
  
  return state;
}

// Custom hook for complex object comparison
function useDeepMemo(factory, deps) {
  const ref = useRef();
  
  if (!ref.current || !deepEqual(ref.current.deps, deps)) {
    ref.current = {
      deps: deps,
      value: factory()
    };
  }
  
  return ref.current.value;
}

// Usage examples
function SearchResults({ query, filters }) {
  // Debounce search query to prevent excessive API calls
  const debouncedQuery = useDebounce(query, 300);
  
  // Memoized search function
  const searchFn = useCallback(async () => {
    if (!debouncedQuery.trim()) return [];
    
    const response = await fetch('/api/search', {
      method: 'POST',
      body: JSON.stringify({ query: debouncedQuery, filters })
    });
    
    return response.json();
  }, [debouncedQuery, filters]);
  
  // Memoized async search with dependency tracking
  const { data: results, loading, error } = useMemoizedAsync(searchFn, [debouncedQuery, filters]);
  
  // Deep memoization for complex filter processing
  const processedFilters = useDeepMemo(() => {
    return Object.entries(filters).reduce((acc, [key, value]) => {
      if (value && value.length > 0) {
        acc[key] = value;
      }
      return acc;
    }, {});
  }, [filters]);
  
  if (loading) return <div>Searching...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return (
    <div>
      <p>Results for "{debouncedQuery}" with {Object.keys(processedFilters).length} filters</p>
      {results?.map(result => (
        <SearchResultItem key={result.id} result={result} />
      ))}
    </div>
  );
}

Code splitting and lazy loading

Reducing initial bundle size dramatically improves perceived performance, especially on slower devices and networks.

Component-level lazy loading

Split components that aren't immediately needed:

import { lazy, Suspense } from 'react';

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

// Lazy load with named exports
const AdminPanel = lazy(() => 
  import('./pages/AdminPanel').then(module => ({ default: module.AdminPanel }))
);

// Loading component for better UX
function LoadingSpinner({ message = 'Loading...' }) {
  return (
    <div className="loading-spinner">
      <div className="spinner" />
      <p>{message}</p>
    </div>
  );
}

// Error boundary for lazy loading failures
class LazyLoadErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    console.error('Lazy loading failed:', error, errorInfo);
  }
  
  render() {
    if (this.state.hasError) {
      return (
        <div className="error-boundary">
          <h2>Failed to load component</h2>
          <button onClick={() => window.location.reload()}>
            Retry
          </button>
        </div>
      );
    }
    
    return this.props.children;
  }
}

// Router with lazy loading
function App() {
  return (
    <Router>
      <Header />
      <main>
        <LazyLoadErrorBoundary>
          <Suspense fallback={<LoadingSpinner message="Loading page..." />}>
            <Routes>
              <Route path="/" element={<Home />} />
              <Route path="/dashboard" element={<Dashboard />} />
              <Route path="/analytics" element={<Analytics />} />
              <Route path="/settings" element={<Settings />} />
              <Route path="/admin" element={<AdminPanel />} />
            </Routes>
          </Suspense>
        </LazyLoadErrorBoundary>
      </main>
    </Router>
  );
}

Dynamic imports for conditional loading

Load features only when needed:

function FeatureModal({ feature, isOpen, onClose }) {
  const [FeatureComponent, setFeatureComponent] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    if (!isOpen || FeatureComponent) return;
    
    setLoading(true);
    setError(null);
    
    // Dynamic import based on feature type
    const loadFeature = async () => {
      try {
        let module;
        
        switch (feature.type) {
          case 'chart':
            module = await import('./features/ChartBuilder');
            break;
          case 'form':
            module = await import('./features/FormBuilder');
            break;
          case 'editor':
            module = await import('./features/RichTextEditor');
            break;
          default:
            throw new Error(`Unknown feature type: ${feature.type}`);
        }
        
        setFeatureComponent(() => module.default);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    
    loadFeature();
  }, [isOpen, feature.type, FeatureComponent]);
  
  if (!isOpen) return null;
  
  return (
    <Modal onClose={onClose}>
      {loading && <LoadingSpinner message={`Loading ${feature.type}...`} />}
      {error && (
        <div className="error">
          <p>Failed to load feature: {error}</p>
          <button onClick={() => setFeatureComponent(null)}>Retry</button>
        </div>
      )}
      {FeatureComponent && (
        <FeatureComponent {...feature.props} onClose={onClose} />
      )}
    </Modal>
  );
}

Optimizing third-party libraries

Minimize the impact of external dependencies:

// Bundle analyzer to identify heavy dependencies
// Run: npm install --save-dev webpack-bundle-analyzer
// Add to package.json: "analyze": "npm run build && npx webpack-bundle-analyzer build/static/js/*.js"

// Lazy load heavy libraries only when needed
function useChartLibrary() {
  const [Chart, setChart] = useState(null);
  
  const loadChart = useCallback(async () => {
    if (Chart) return Chart;
    
    // Only load Chart.js when actually needed
    const { Chart: ChartClass, registerables } = await import('chart.js');
    ChartClass.register(...registerables);
    
    setChart(ChartClass);
    return ChartClass;
  }, [Chart]);
  
  return { Chart, loadChart };
}

// Tree-shakable utility imports
// ❌ Imports entire lodash library
import _ from 'lodash';

// ✅ Import only needed functions
import { debounce, throttle } from 'lodash';
// or even better, import specific modules
import debounce from 'lodash/debounce';
import throttle from 'lodash/throttle';

// Replace heavy libraries with lighter alternatives
// ❌ moment.js (67KB gzipped)
import moment from 'moment';

// ✅ date-fns (2-20KB gzipped, depending on what you import)
import { format, parseISO, addDays } from 'date-fns';

// Create lightweight wrappers for heavy operations
function useImageProcessing() {
  const processImage = useCallback(async (file) => {
    // Load image processing library only when needed
    const { default: imageCompression } = await import('browser-image-compression');
    
    const options = {
      maxSizeMB: 1,
      maxWidthOrHeight: 1920,
      useWebWorker: true
    };
    
    return await imageCompression(file, options);
  }, []);
  
  return { processImage };
}

Bundle optimization strategies

Optimizing your JavaScript bundle size directly impacts load times and user experience.

Webpack optimization configuration

Configure your build process for optimal performance:

// webpack.config.js or next.config.js
module.exports = {
  // Enable production optimizations
  mode: 'production',
  
  optimization: {
    // Split chunks for better caching
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        // Vendor libraries bundle
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10
        },
        // Common components bundle
        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
          priority: 5,
          reuseExistingChunk: true
        }
      }
    },
    
    // Minimize bundle size
    usedExports: true,
    sideEffects: false,
    
    // Runtime chunk for better caching
    runtimeChunk: {
      name: 'runtime'
    }
  },
  
  // Resolve configuration for tree shaking
  resolve: {
    // Prefer ES modules for better tree shaking
    mainFields: ['browser', 'module', 'main'],
    
    // Alias heavy libraries to lighter alternatives
    alias: {
      'moment': 'date-fns',
      'lodash': 'lodash-es'
    }
  },
  
  // Module rules for optimization
  module: {
    rules: [
      {
        test: /\.(js|jsx|ts|tsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env', {
                modules: false, // Preserve ES modules for tree shaking
                useBuiltIns: 'usage',
                corejs: 3
              }],
              ['@babel/preset-react', {
                runtime: 'automatic' // Use new JSX transform
              }],
              '@babel/preset-typescript'
            ],
            plugins: [
              // Remove unused imports
              'babel-plugin-transform-remove-console', // Remove console.logs in production
              '@babel/plugin-syntax-dynamic-import'
            ]
          }
        }
      }
    ]
  },
  
  // Plugins for optimization
  plugins: [
    // Analyze bundle size
    new (require('webpack-bundle-analyzer').BundleAnalyzerPlugin)({
      analyzerMode: process.env.ANALYZE ? 'server' : 'disabled'
    }),
    
    // Define environment variables
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production')
    })
  ]
};

Tree shaking optimization

Ensure your code is tree-shakable:

// utils/index.js - ❌ Barrel exports prevent tree shaking
export { debounce } from './debounce';
export { throttle } from './throttle';
export { formatDate } from './date';
export { validateEmail } from './validation';

// ❌ This imports all utilities even if you only need one
import { debounce } from './utils';

// ✅ utils/debounce.js - Direct exports for tree shaking
export function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

// ✅ Import directly for better tree shaking
import { debounce } from './utils/debounce';

// Make functions tree-shakable with proper ES modules
// ❌ CommonJS module (not tree-shakable)
module.exports = {
  debounce: function(func, wait) { /* ... */ },
  throttle: function(func, wait) { /* ... */ }
};

// ✅ ES module with named exports (tree-shakable)
export function debounce(func, wait) { /* ... */ }
export function throttle(func, wait) { /* ... */ }

// Mark side effects in package.json
{
  "name": "my-app",
  "sideEffects": [
    "*.css",
    "*.scss",
    "./src/polyfills.js"
  ]
}

Image optimization and asset loading

Optimize image loading for better performance:

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

// Lazy loading image component with WebP support
function OptimizedImage({ 
  src, 
  webpSrc, 
  alt, 
  width, 
  height, 
  className,
  placeholder = 'blur',
  loading = 'lazy'
}) {
  const [isLoaded, setIsLoaded] = useState(false);
  const [isInView, setIsInView] = useState(false);
  const imgRef = useRef();
  
  // Intersection Observer for lazy loading
  useEffect(() => {
    if (!imgRef.current) return;
    
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsInView(true);
          observer.disconnect();
        }
      },
      { threshold: 0.1 }
    );
    
    observer.observe(imgRef.current);
    
    return () => observer.disconnect();
  }, []);
  
  // Generate responsive srcSet
  const generateSrcSet = (baseSrc, format = 'jpg') => {
    const sizes = [320, 640, 768, 1024, 1280, 1920];
    return sizes
      .map(size => `${baseSrc}?w=${size}&f=${format} ${size}w`)
      .join(', ');
  };
  
  return (
    <div 
      ref={imgRef}
      className={`image-container ${className}`}
      style={{ 
        width, 
        height,
        backgroundColor: placeholder === 'blur' ? '#f0f0f0' : 'transparent'
      }}
    >
      {(isInView || loading !== 'lazy') && (
        <picture>
          {/* WebP format for modern browsers */}
          {webpSrc && (
            <source
              srcSet={generateSrcSet(webpSrc, 'webp')}
              sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
              type="image/webp"
            />
          )}
          
          {/* Fallback format */}
          <img
            src={src}
            srcSet={generateSrcSet(src)}
            sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
            alt={alt}
            width={width}
            height={height}
            loading={loading}
            onLoad={() => setIsLoaded(true)}
            style={{
              transition: 'opacity 0.3s ease',
              opacity: isLoaded ? 1 : 0
            }}
          />
        </picture>
      )}
      
      {/* Loading placeholder */}
      {!isLoaded && (
        <div className="image-placeholder">
          <div className="loading-skeleton" />
        </div>
      )}
    </div>
  );
}

// Progressive image loading with blur effect
function ProgressiveImage({ lowQualitySrc, highQualitySrc, alt, ...props }) {
  const [highQualityLoaded, setHighQualityLoaded] = useState(false);
  
  return (
    <div className="progressive-image" {...props}>
      {/* Low quality placeholder */}
      <img
        src={lowQualitySrc}
        alt={alt}
        className={`low-quality ${highQualityLoaded ? 'fade-out' : ''}`}
        style={{
          filter: 'blur(5px)',
          transition: 'opacity 0.3s ease'
        }}
      />
      
      {/* High quality image */}
      <img
        src={highQualitySrc}
        alt={alt}
        className={`high-quality ${highQualityLoaded ? 'fade-in' : ''}`}
        onLoad={() => setHighQualityLoaded(true)}
        style={{
          position: 'absolute',
          top: 0,
          left: 0,
          opacity: highQualityLoaded ? 1 : 0,
          transition: 'opacity 0.3s ease'
        }}
      />
    </div>
  );
}

Modern React concurrent features

React 18 introduced concurrent features that can significantly improve user experience through better prioritization of updates.

Suspense for data fetching

Implement Suspense boundaries for smoother loading states:

import { Suspense } from 'react';

// Data fetching component that integrates with Suspense
function UserProfile({ userId }) {
  // Using a library like SWR or React Query that supports Suspense
  const user = useSuspenseQuery(['user', userId], () => 
    fetch(`/api/users/${userId}`).then(res => res.json())
  );
  
  return (
    <div className="user-profile">
      <img src={user.avatar} alt={user.name} />
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

function UserComments({ userId }) {
  const comments = useSuspenseQuery(['comments', userId], () =>
    fetch(`/api/users/${userId}/comments`).then(res => res.json())
  );
  
  return (
    <div className="user-comments">
      <h3>Recent Comments</h3>
      {comments.map(comment => (
        <div key={comment.id} className="comment">
          <p>{comment.content}</p>
          <small>{comment.createdAt}</small>
        </div>
      ))}
    </div>
  );
}

// Nested Suspense boundaries for progressive loading
function UserPage({ userId }) {
  return (
    <div className="user-page">
      <Suspense fallback={<UserProfileSkeleton />}>
        <UserProfile userId={userId} />
        
        <Suspense fallback={<CommentsSkeleton />}>
          <UserComments userId={userId} />
        </Suspense>
      </Suspense>
    </div>
  );
}

// Skeleton loading components
function UserProfileSkeleton() {
  return (
    <div className="user-profile skeleton">
      <div className="skeleton-avatar" />
      <div className="skeleton-text skeleton-title" />
      <div className="skeleton-text skeleton-subtitle" />
    </div>
  );
}

function CommentsSkeleton() {
  return (
    <div className="user-comments skeleton">
      <div className="skeleton-text skeleton-title" />
      {[...Array(3)].map((_, i) => (
        <div key={i} className="skeleton-comment">
          <div className="skeleton-text" />
          <div className="skeleton-text skeleton-small" />
        </div>
      ))}
    </div>
  );
}

Concurrent rendering with useTransition

Prioritize important updates over less critical ones:

import { useState, useTransition, useDeferredValue } from 'react';

function SearchInterface() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();
  
  // Defer expensive operations
  const deferredQuery = useDeferredValue(query);
  
  const handleSearch = (newQuery) => {
    // Update input immediately (high priority)
    setQuery(newQuery);
    
    // Defer search results update (low priority)
    startTransition(() => {
      // This update won't block the input from updating
      performExpensiveSearch(newQuery).then(setResults);
    });
  };
  
  return (
    <div className="search-interface">
      <input
        type="text"
        value={query}
        onChange={(e) => handleSearch(e.target.value)}
        placeholder="Search..."
        className={isPending ? 'searching' : ''}
      />
      
      {isPending && <div className="search-indicator">Searching...</div>}
      
      <SearchResults 
        results={results} 
        query={deferredQuery}
        isPending={isPending}
      />
    </div>
  );
}

// Heavy list component that benefits from concurrent features
function SearchResults({ results, query, isPending }) {
  const processedResults = useMemo(() => {
    return results.map(result => ({
      ...result,
      highlighted: highlightMatches(result.content, query),
      relevanceScore: calculateRelevance(result, query)
    }));
  }, [results, query]);
  
  return (
    <div className={`search-results ${isPending ? 'updating' : ''}`}>
      {processedResults.map(result => (
        <SearchResultItem 
          key={result.id} 
          result={result}
          isPending={isPending}
        />
      ))}
    </div>
  );
}

Error boundaries with concurrent features

Handle errors gracefully in concurrent applications:

class ConcurrentErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { 
      hasError: false, 
      error: null,
      errorInfo: null 
    };
  }
  
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  
  componentDidCatch(error, errorInfo) {
    this.setState({ errorInfo });
    
    // Log error with additional context for concurrent features
    console.error('Concurrent feature error:', {
      error: error.message,
      stack: error.stack,
      errorInfo,
      isConcurrent: true,
      timestamp: new Date().toISOString()
    });
  }
  
  handleRetry = () => {
    this.setState({ 
      hasError: false, 
      error: null, 
      errorInfo: null 
    });
  };
  
  render() {
    if (this.state.hasError) {
      return (
        <div className="error-boundary">
          <h2>Something went wrong</h2>
          <details>
            <summary>Error details</summary>
            <pre>{this.state.error?.message}</pre>
            <pre>{this.state.errorInfo?.componentStack}</pre>
          </details>
          <button onClick={this.handleRetry}>Try Again</button>
        </div>
      );
    }
    
    return this.props.children;
  }
}

// Usage with Suspense and concurrent features
function App() {
  return (
    <ConcurrentErrorBoundary>
      <Suspense fallback={<AppSkeleton />}>
        <Header />
        <main>
          <Suspense fallback={<ContentSkeleton />}>
            <MainContent />
          </Suspense>
        </main>
        <Footer />
      </Suspense>
    </ConcurrentErrorBoundary>
  );
}

Performance monitoring and optimization workflow

Building on the TypeScript patterns from our TypeScript React guide, let's implement comprehensive performance monitoring.

Real-time performance monitoring

Track performance metrics in production:

interface PerformanceMetrics {
  componentName: string;
  renderTime: number;
  renderCount: number;
  memoryUsage: number;
  timestamp: number;
}

class PerformanceMonitor {
  private metrics: Map<string, PerformanceMetrics> = new Map();
  private observers: Map<string, PerformanceObserver> = new Map();
  
  startMonitoring(componentName: string) {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.name.includes(componentName)) {
          this.recordMetric(componentName, {
            renderTime: entry.duration,
            memoryUsage: (performance as any).memory?.usedJSHeapSize || 0,
            timestamp: entry.startTime
          });
        }
      }
    });
    
    observer.observe({ entryTypes: ['measure'] });
    this.observers.set(componentName, observer);
  }
  
  stopMonitoring(componentName: string) {
    const observer = this.observers.get(componentName);
    if (observer) {
      observer.disconnect();
      this.observers.delete(componentName);
    }
  }
  
  private recordMetric(componentName: string, data: Partial<PerformanceMetrics>) {
    const existing = this.metrics.get(componentName);
    const updated: PerformanceMetrics = {
      componentName,
      renderTime: data.renderTime || 0,
      renderCount: (existing?.renderCount || 0) + 1,
      memoryUsage: data.memoryUsage || 0,
      timestamp: data.timestamp || performance.now()
    };
    
    this.metrics.set(componentName, updated);
    
    // Send alerts for performance issues
    if (updated.renderTime > 16 || updated.renderCount > 100) {
      this.sendPerformanceAlert(updated);
    }
  }
  
  private sendPerformanceAlert(metrics: PerformanceMetrics) {
    console.warn('Performance issue detected:', metrics);
    
    // Send to analytics service
    if (typeof window !== 'undefined' && window.gtag) {
      window.gtag('event', 'performance_issue', {
        component_name: metrics.componentName,
        render_time: metrics.renderTime,
        render_count: metrics.renderCount
      });
    }
  }
  
  getMetrics(): PerformanceMetrics[] {
    return Array.from(this.metrics.values());
  }
  
  getSlowComponents(threshold: number = 16): PerformanceMetrics[] {
    return this.getMetrics().filter(metric => metric.renderTime > threshold);
  }
}

const performanceMonitor = new PerformanceMonitor();

// Performance monitoring hook
function usePerformanceMonitoring(componentName: string) {
  const renderStartTime = useRef<number>(0);
  
  // Start timing before render
  renderStartTime.current = performance.now();
  
  useEffect(() => {
    performanceMonitor.startMonitoring(componentName);
    
    // Mark render complete
    const renderTime = performance.now() - renderStartTime.current;
    performance.mark(`${componentName}-render-end`);
    performance.measure(`${componentName}-render`, `${componentName}-render-start`, `${componentName}-render-end`);
    
    return () => {
      performanceMonitor.stopMonitoring(componentName);
    };
  }, [componentName]);
  
  // Mark render start
  performance.mark(`${componentName}-render-start`);
  
  return {
    getMetrics: () => performanceMonitor.getMetrics(),
    getSlowComponents: () => performanceMonitor.getSlowComponents()
  };
}

Automated performance testing

Implement performance regression detection:

// Performance test utility
class PerformanceTestSuite {
  constructor() {
    this.tests = new Map();
    this.baselines = new Map();
  }
  
  addTest(name, testFn, baseline = null) {
    this.tests.set(name, testFn);
    if (baseline) {
      this.baselines.set(name, baseline);
    }
  }
  
  async runTests() {
    const results = new Map();
    
    for (const [name, testFn] of this.tests) {
      console.log(`Running performance test: ${name}`);
      
      const startTime = performance.now();
      await testFn();
      const endTime = performance.now();
      
      const duration = endTime - startTime;
      const baseline = this.baselines.get(name);
      
      results.set(name, {
        duration,
        baseline,
        passed: baseline ? duration <= baseline * 1.1 : true, // 10% tolerance
        regression: baseline ? duration / baseline : 1
      });
    }
    
    return results;
  }
  
  generateReport(results) {
    const report = {
      timestamp: new Date().toISOString(),
      tests: Array.from(results.entries()).map(([name, result]) => ({
        name,
        duration: Math.round(result.duration * 100) / 100,
        baseline: result.baseline,
        passed: result.passed,
        regression: Math.round(result.regression * 100) / 100
      })),
      summary: {
        total: results.size,
        passed: Array.from(results.values()).filter(r => r.passed).length,
        regressions: Array.from(results.values()).filter(r => r.regression > 1.1).length
      }
    };
    
    console.table(report.tests);
    return report;
  }
}

// Example performance tests
const perfTests = new PerformanceTestSuite();

perfTests.addTest('large-list-render', async () => {
  const { render } = await import('@testing-library/react');
  const LargeList = await import('./components/LargeList');
  
  const items = Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `Item ${i}` }));
  render(<LargeList items={items} />);
}, 50); // 50ms baseline

perfTests.addTest('search-filtering', async () => {
  const { render, fireEvent } = await import('@testing-library/react');
  const SearchComponent = await import('./components/SearchComponent');
  
  const items = Array.from({ length: 5000 }, (_, i) => ({ 
    id: i, 
    name: `Item ${i}`,
    description: `Description for item ${i}`
  }));
  
  const { getByPlaceholderText } = render(<SearchComponent items={items} />);
  const searchInput = getByPlaceholderText('Search...');
  
  fireEvent.change(searchInput, { target: { value: 'test search query' } });
}, 100); // 100ms baseline

// Run tests in CI/CD pipeline
if (process.env.NODE_ENV === 'test') {
  perfTests.runTests().then(results => {
    const report = perfTests.generateReport(results);
    
    // Fail CI if performance regressions detected
    if (report.summary.regressions > 0) {
      console.error('Performance regressions detected!');
      process.exit(1);
    }
  });
}

Building production-ready optimized applications

For businesses scaling their React applications, systematic performance optimization becomes critical for user retention and conversion rates.

Performance optimization checklist

Implement this checklist for production applications:

// Performance audit component for development
function PerformanceAudit({ children }) {
  const [audit, setAudit] = useState(null);
  
  useEffect(() => {
    if (process.env.NODE_ENV !== 'development') return;
    
    const runAudit = async () => {
      const results = {
        bundleSize: await checkBundleSize(),
        renderPerformance: await checkRenderPerformance(),
        memoryUsage: checkMemoryUsage(),
        imageOptimization: await checkImageOptimization(),
        accessibilityScore: await checkAccessibility()
      };
      
      setAudit(results);
    };
    
    // Run audit after component tree is stable
    setTimeout(runAudit, 2000);
  }, []);
  
  if (process.env.NODE_ENV !== 'development') {
    return children;
  }
  
  return (
    <>
      {children}
      {audit && (
        <div className="performance-audit">
          <h3>Performance Audit Results</h3>
          <ul>
            <li className={audit.bundleSize.passed ? 'pass' : 'fail'}>
              Bundle Size: {audit.bundleSize.size}KB (Budget: {audit.bundleSize.budget}KB)
            </li>
            <li className={audit.renderPerformance.passed ? 'pass' : 'fail'}>
              Render Performance: {audit.renderPerformance.averageTime}ms
            </li>
            <li className={audit.memoryUsage.passed ? 'pass' : 'fail'}>
              Memory Usage: {audit.memoryUsage.current}MB (Budget: {audit.memoryUsage.budget}MB)
            </li>
            <li className={audit.imageOptimization.passed ? 'pass' : 'fail'}>
              Images Optimized: {audit.imageOptimization.optimized}/{audit.imageOptimization.total}
            </li>
            <li className={audit.accessibilityScore.passed ? 'pass' : 'fail'}>
              Accessibility Score: {audit.accessibilityScore.score}/100
            </li>
          </ul>
        </div>
      )}
    </>
  );
}

async function checkBundleSize() {
  // Check if bundle analyzer report exists
  try {
    const response = await fetch('/bundle-stats.json');
    const stats = await response.json();
    const totalSize = stats.assets.reduce((sum, asset) => sum + asset.size, 0) / 1024;
    
    return {
      size: Math.round(totalSize),
      budget: 250,
      passed: totalSize <= 250
    };
  } catch {
    return { size: 'Unknown', budget: 250, passed: false };
  }
}

async function checkRenderPerformance() {
  const entries = performance.getEntriesByType('measure');
  const renderTimes = entries
    .filter(entry => entry.name.includes('render'))
    .map(entry => entry.duration);
  
  const averageTime = renderTimes.length > 0 
    ? renderTimes.reduce((sum, time) => sum + time, 0) / renderTimes.length 
    : 0;
  
  return {
    averageTime: Math.round(averageTime * 100) / 100,
    budget: 16,
    passed: averageTime <= 16
  };
}

function checkMemoryUsage() {
  const memory = (performance as any).memory;
  if (!memory) {
    return { current: 'Unknown', budget: 50, passed: false };
  }
  
  const currentMB = Math.round(memory.usedJSHeapSize / 1024 / 1024);
  
  return {
    current: currentMB,
    budget: 50,
    passed: currentMB <= 50
  };
}

Deployment optimization strategies

Optimize your deployment for maximum performance:

// next.config.js - Production optimization
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({
  // Enable SWC minification
  swcMinify: true,
  
  // Optimize images
  images: {
    formats: ['image/webp', 'image/avif'],
    minimumCacheTTL: 31536000, // 1 year
    dangerouslyAllowSVG: false,
    contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
  },
  
  // Compress responses
  compress: true,
  
  // Optimize fonts
  optimizeFonts: true,
  
  // Webpack optimizations
  webpack: (config, { dev, isServer }) => {
    if (!dev && !isServer) {
      // Split chunks for better caching
      config.optimization.splitChunks = {
        chunks: 'all',
        cacheGroups: {
          default: false,
          vendors: false,
          // Vendor chunk
          vendor: {
            name: 'vendor',
            chunks: 'all',
            test: /node_modules/,
            priority: 20
          },
          // Common chunk
          common: {
            name: 'common',
            minChunks: 2,
            chunks: 'all',
            priority: 10,
            reuseExistingChunk: true,
            enforce: true
          }
        }
      };
      
      // Remove console logs in production
      config.optimization.minimizer[0].options.minimizer.options.compress.drop_console = true;
    }
    
    return config;
  },
  
  // Headers for caching
  async headers() {
    return [
      {
        source: '/_next/static/(.*)',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable'
          }
        ]
      },
      {
        source: '/images/(.*)',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000'
          }
        ]
      }
    ];
  },
  
  // Experimental features for performance
  experimental: {
    // Server components for reduced bundle size
    serverComponents: true,
    // Concurrent features
    concurrentFeatures: true,
    // Runtime optimizations
    runtime: 'nodejs',
    // ISR improvements
    isrMemoryCacheSize: 0, // Disable ISR memory cache for Vercel
  }
});

When you're ready to implement these performance optimization strategies in your production applications, our team specializes in building high-performance web applications that maintain fast load times and smooth user experiences as they scale.

Measuring success and continuous optimization

Performance optimization is an ongoing process that requires systematic measurement and iteration.

Key performance indicators

Track these metrics to measure optimization success:

// Performance KPI tracking
class PerformanceKPITracker {
  constructor() {
    this.metrics = {
      // Core Web Vitals
      LCP: [], // Largest Contentful Paint
      FID: [], // First Input Delay
      CLS: [], // Cumulative Layout Shift
      
      // Custom metrics
      TTI: [], // Time to Interactive
      TBT: [], // Total Blocking Time
      renderCount: new Map(),
      memoryUsage: [],
      bundleSize: 0
    };
  }
  
  trackCoreWebVitals() {
    // Track LCP
    new PerformanceObserver((list) => {
      const entries = list.getEntries();
      const lastEntry = entries[entries.length - 1];
      this.metrics.LCP.push(lastEntry.startTime);
      this.reportMetric('LCP', lastEntry.startTime);
    }).observe({ entryTypes: ['largest-contentful-paint'] });
    
    // Track FID
    new PerformanceObserver((list) => {
      const entries = list.getEntries();
      entries.forEach(entry => {
        this.metrics.FID.push(entry.processingStart - entry.startTime);
        this.reportMetric('FID', entry.processingStart - entry.startTime);
      });
    }).observe({ entryTypes: ['first-input'] });
    
    // Track CLS
    let clsValue = 0;
    new PerformanceObserver((list) => {
      const entries = list.getEntries();
      entries.forEach(entry => {
        if (!entry.hadRecentInput) {
          clsValue += entry.value;
        }
      });
      this.metrics.CLS.push(clsValue);
      this.reportMetric('CLS', clsValue);
    }).observe({ entryTypes: ['layout-shift'] });
  }
  
  reportMetric(name, value) {
    // Define thresholds
    const thresholds = {
      LCP: { good: 2500, poor: 4000 },
      FID: { good: 100, poor: 300 },
      CLS: { good: 0.1, poor: 0.25 }
    };
    
    const threshold = thresholds[name];
    if (!threshold) return;
    
    const status = value <= threshold.good ? 'good' : 
                  value <= threshold.poor ? 'needs-improvement' : 'poor';
    
    console.log(`${name}: ${value} (${status})`);
    
    // Send to analytics
    if (typeof gtag !== 'undefined') {
      gtag('event', 'web_vitals', {
        metric_name: name,
        metric_value: Math.round(value),
        metric_rating: status
      });
    }
  }
  
  generateReport() {
    return {
      timestamp: new Date().toISOString(),
      coreWebVitals: {
        LCP: this.calculateP75(this.metrics.LCP),
        FID: this.calculateP75(this.metrics.FID),
        CLS: this.calculateP75(this.metrics.CLS)
      },
      customMetrics: {
        averageRenderCount: this.calculateAverageRenderCount(),
        peakMemoryUsage: Math.max(...this.metrics.memoryUsage),
        bundleSize: this.metrics.bundleSize
      }
    };
  }
  
  calculateP75(values) {
    if (values.length === 0) return 0;
    const sorted = values.sort((a, b) => a - b);
    const index = Math.ceil(sorted.length * 0.75) - 1;
    return sorted[index];
  }
  
  calculateAverageRenderCount() {
    const counts = Array.from(this.metrics.renderCount.values());
    return counts.length > 0 ? counts.reduce((a, b) => a + b, 0) / counts.length : 0;
  }
}

// Initialize tracking
const kpiTracker = new PerformanceKPITracker();
kpiTracker.trackCoreWebVitals();

Automated performance monitoring

Set up continuous performance monitoring:

// Performance monitoring service
class PerformanceMonitoringService {
  constructor(config) {
    this.config = {
      alertThresholds: {
        renderTime: 50,
        memoryIncrease: 10,
        bundleSizeIncrease: 20
      },
      reportingInterval: 60000, // 1 minute
      ...config
    };
    
    this.baseline = null;
    this.currentMetrics = {};
    this.alerts = [];
  }
  
  start() {
    this.collectBaseline();
    this.startMonitoring();
    this.startReporting();
  }
  
  collectBaseline() {
    // Collect baseline metrics on first load
    setTimeout(() => {
      this.baseline = {
        renderTime: this.measureAverageRenderTime(),
        memoryUsage: this.getCurrentMemoryUsage(),
        bundleSize: this.getBundleSize()
      };
      
      console.log('Performance baseline established:', this.baseline);
    }, 5000);
  }
  
  startMonitoring() {
    setInterval(() => {
      const current = {
        renderTime: this.measureAverageRenderTime(),
        memoryUsage: this.getCurrentMemoryUsage(),
        bundleSize: this.getBundleSize()
      };
      
      this.currentMetrics = current;
      this.checkForAlerts(current);
    }, this.config.reportingInterval);
  }
  
  checkForAlerts(current) {
    if (!this.baseline) return;
    
    const alerts = [];
    
    // Check render time increase
    const renderIncrease = ((current.renderTime - this.baseline.renderTime) / this.baseline.renderTime) * 100;
    if (renderIncrease > this.config.alertThresholds.renderTime) {
      alerts.push({
        type: 'render_performance',
        message: `Render time increased by ${renderIncrease.toFixed(1)}%`,
        severity: 'warning',
        current: current.renderTime,
        baseline: this.baseline.renderTime
      });
    }
    
    // Check memory usage increase
    const memoryIncrease = ((current.memoryUsage - this.baseline.memoryUsage) / this.baseline.memoryUsage) * 100;
    if (memoryIncrease > this.config.alertThresholds.memoryIncrease) {
      alerts.push({
        type: 'memory_usage',
        message: `Memory usage increased by ${memoryIncrease.toFixed(1)}%`,
        severity: 'error',
        current: current.memoryUsage,
        baseline: this.baseline.memoryUsage
      });
    }
    
    this.alerts.push(...alerts);
    alerts.forEach(alert => this.sendAlert(alert));
  }
  
  sendAlert(alert) {
    console.warn('Performance Alert:', alert);
    
    // Send to monitoring service
    if (typeof fetch !== 'undefined') {
      fetch('/api/performance-alerts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          ...alert,
          timestamp: new Date().toISOString(),
          userAgent: navigator.userAgent,
          url: window.location.href
        })
      }).catch(console.error);
    }
  }
  
  measureAverageRenderTime() {
    const entries = performance.getEntriesByType('measure');
    const renderTimes = entries
      .filter(entry => entry.name.includes('render'))
      .map(entry => entry.duration);
    
    return renderTimes.length > 0 
      ? renderTimes.reduce((sum, time) => sum + time, 0) / renderTimes.length 
      : 0;
  }
  
  getCurrentMemoryUsage() {
    const memory = (performance as any).memory;
    return memory ? memory.usedJSHeapSize / 1024 / 1024 : 0;
  }
  
  getBundleSize() {
    // Estimate based on loaded scripts
    const scripts = Array.from(document.querySelectorAll('script[src]'));
    return scripts.length * 50; // Rough estimate
  }
}

// Initialize monitoring
const monitoring = new PerformanceMonitoringService({
  alertThresholds: {
    renderTime: 30, // 30% increase
    memoryIncrease: 15, // 15% increase
    bundleSizeIncrease: 25 // 25% increase
  }
});

if (typeof window !== 'undefined') {
  monitoring.start();
}

Building performance-optimized React applications

React performance optimization is about making deliberate architectural decisions that prioritize user experience. The techniques we've explored - from preventing unnecessary re-renders to implementing advanced code splitting - create applications that remain responsive as they scale.

The key insight is that performance optimization isn't a one-time task but an ongoing discipline. By implementing systematic monitoring, establishing performance budgets, and using tools like React DevTools Profiler, you create a feedback loop that prevents performance debt from accumulating.

The patterns we've covered integrate seamlessly with modern development workflows. From the memoization techniques that build on our React hooks patterns to the type-safe monitoring systems that leverage TypeScript's benefits, these optimizations compound to create applications that users love to interact with.

Whether you're optimizing an existing React application or building performance into a new project from the ground up, the systematic approach we've outlined provides measurable improvements in user experience metrics that directly impact business outcomes.

Ready to implement these performance optimization strategies in your React application? Start your project brief to discuss how we can help you build applications that remain fast and responsive as your user base grows.

React performance optimization - FAQ & implementation guide

Use React.memo when a component receives the same props frequently but renders expensive content, when parent components re-render often, or when you have large lists of similar components. Always profile first to confirm the optimization is needed.

useMemo memoizes the result of a computation and returns the cached value, while useCallback memoizes the function itself. Use useMemo for expensive calculations and useCallback for stable function references to prevent child re-renders.

Lazy loading splits your application into smaller chunks that load on-demand, reducing initial bundle size and improving page load times. Use React.lazy() for route-based splitting and dynamic imports for component-level optimization.

Common bottlenecks include unnecessary re-renders, large bundle sizes, unoptimized images, expensive computations on every render, and inefficient list rendering. Use React DevTools Profiler to identify actual performance issues.

Use React DevTools Profiler for component-level analysis, Chrome DevTools for general performance, Lighthouse for user experience metrics, and tools like Bundle Analyzer for bundle size optimization.

No, premature optimization can harm code readability and maintainability. Profile your application first, identify actual bottlenecks, then optimize components that cause measurable performance issues. Focus on user-perceived performance improvements.

Stay ahead with expert insights

Get practical tips on web design, business growth, SEO strategies, and development best practices delivered to your inbox.