React & Next.js Topic

React Performance Optimization

1. React.memo - Preventing Unnecessary Re-renders

Basic Usage

jsx

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

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

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

// Custom comparison function
const DeepCompareComponent = memo(({ user, config }) => {
  console.log('DeepCompareComponent rendered');
  return (
    <div>
      <h3>{user.name}</h3>
      <p>Theme: {config.theme}</p>
    </div>
  );
}, (prevProps, nextProps) => {
  // Custom comparison - return true if props are equal (no re-render)
  return (
    prevProps.user.id === nextProps.user.id &&
    prevProps.config.theme === nextProps.config.theme
  );
});

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState({ id: 1, name: 'John', email: 'john@example.com' });
  const [theme, setTheme] = useState('light');

  const handleClick = () => {
    console.log('Button clicked');
  };

  // This would cause unnecessary re-renders without useCallback
  const stableHandleClick = React.useCallback(() => {
    console.log('Button clicked');
  }, []);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Re-render Parent ({count})
      </button>
      
      <button onClick={() => setUser({ ...user, name: 'John Updated' })}>
        Update User
      </button>

      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>

      {/* This re-renders every time parent re-renders */}
      <RegularComponent user={user} onClick={handleClick} />
      
      {/* This only re-renders when user prop changes */}
      <OptimizedComponent user={user} onClick={stableHandleClick} />
      
      {/* Custom comparison for complex props */}
      <DeepCompareComponent 
        user={user} 
        config={{ theme }} 
      />
    </div>
  );
}

When to Use React.memo

jsx

import React, { memo } from 'react';

// Good candidates for memo:
// - Components that render often
// - Components with expensive rendering
// - Pure functional components
// - Components that receive the same props frequently

const UserProfile = memo(({ user, onEdit }) => {
  // Expensive calculation
  const formattedUser = React.useMemo(() => {
    console.log('Formatting user...');
    return {
      ...user,
      displayName: `${user.firstName} ${user.lastName}`.trim(),
      joined: new Date(user.joinDate).toLocaleDateString()
    };
  }, [user]);

  return (
    <div className="user-profile">
      <h2>{formattedUser.displayName}</h2>
      <p>Joined: {formattedUser.joined}</p>
      <button onClick={onEdit}>Edit</button>
    </div>
  );
});

// Avoid memo for:
// - Components that always receive different props
// - Very simple components
// - Components that need to re-render frequently anyway

const SimpleButton = ({ children, onClick }) => (
  <button onClick={onClick}>{children}</button>
);

2. Lazy Loading + Suspense

Basic Route-based Code Splitting

jsx

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

// Lazy load pages
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));
const Dashboard = lazy(() => import('./pages/Dashboard'));

// Loading component
const LoadingFallback = () => (
  <div className="loading-spinner">
    <div className="spinner"></div>
    <p>Loading...</p>
  </div>
);

// Error boundary for lazy components
class LazyComponentErrorBoundary extends React.Component {
  state = { hasError: false };

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

  componentDidCatch(error, errorInfo) {
    console.error('Lazy component error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-fallback">
          <h2>Something went wrong while loading this page.</h2>
          <button onClick={() => this.setState({ hasError: false })}>
            Try Again
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

function App() {
  return (
    <Router>
      <div className="app">
        <LazyComponentErrorBoundary>
          <Suspense fallback={<LoadingFallback />}>
            <Routes>
              <Route path="/" element={<Home />} />
              <Route path="/about" element={<About />} />
              <Route path="/contact" element={<Contact />} />
              <Route path="/dashboard/*" element={<Dashboard />} />
            </Routes>
          </Suspense>
        </LazyComponentErrorBoundary>
      </div>
    </Router>
  );
}

Component-level Lazy Loading

jsx

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

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

// Preload components
const preloadComponents = {
  chart: () => import('./components/HeavyChart'),
  grid: () => import('./components/DataGrid'),
  editor: () => import('./components/RichTextEditor')
};

function AnalyticsDashboard() {
  const [activeTab, setActiveTab] = useState('overview');
  const [preloaded, setPreloaded] = useState(new Set());

  const preloadComponent = (componentName) => {
    if (!preloaded.has(componentName) && preloadComponents[componentName]) {
      preloadComponents[componentName]();
      setPreloaded(prev => new Set(prev).add(componentName));
    }
  };

  const renderTabContent = () => {
    switch (activeTab) {
      case 'charts':
        return (
          <Suspense fallback={<ChartLoader />}>
            <HeavyChart />
          </Suspense>
        );
      case 'data':
        return (
          <Suspense fallback={<GridLoader />}>
            <DataGrid />
          </Suspense>
        );
      case 'editor':
        return (
          <Suspense fallback={<EditorLoader />}>
            <RichTextEditor />
          </Suspense>
        );
      default:
        return <Overview />;
    }
  };

  return (
    <div className="dashboard">
      <nav className="tabs">
        <button 
          onClick={() => setActiveTab('overview')}
          className={activeTab === 'overview' ? 'active' : ''}
        >
          Overview
        </button>
        <button 
          onClick={() => {
            setActiveTab('charts');
            preloadComponent('chart');
          }}
          onMouseEnter={() => preloadComponent('chart')}
          className={activeTab === 'charts' ? 'active' : ''}
        >
          Charts
        </button>
        <button 
          onClick={() => {
            setActiveTab('data');
            preloadComponent('grid');
          }}
          onMouseEnter={() => preloadComponent('grid')}
          className={activeTab === 'data' ? 'active' : ''}
        >
          Data Grid
        </button>
        <button 
          onClick={() => {
            setActiveTab('editor');
            preloadComponent('editor');
          }}
          onMouseEnter={() => preloadComponent('editor')}
          className={activeTab === 'editor' ? 'active' : ''}
        >
          Editor
        </button>
      </nav>
      
      <div className="tab-content">
        {renderTabContent()}
      </div>
    </div>
  );
}

// Custom loading components
const ChartLoader = () => (
  <div className="loader">
    <div className="chart-skeleton">
      <div className="skeleton-bar"></div>
      <div className="skeleton-bar"></div>
      <div className="skeleton-bar"></div>
    </div>
  </div>
);

const GridLoader = () => (
  <div className="loader">
    {Array.from({ length: 5 }).map((_, i) => (
      <div key={i} className="skeleton-row"></div>
    ))}
  </div>
);

const EditorLoader = () => (
  <div className="loader">
    <div className="skeleton-toolbar"></div>
    <div className="skeleton-editor"></div>
  </div>
);

3. Avoiding Unnecessary Re-renders

Proper useCallback and useMemo

jsx

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

// Optimized parent component
function UserManagement() {
  const [users, setUsers] = useState([]);
  const [filter, setFilter] = useState('');
  const [sortBy, setSortBy] = useState('name');

  // Bad - recreates function on every render
  const badAddUser = (user) => {
    setUsers(prev => [...prev, user]);
  };

  // Good - stable function reference
  const addUser = useCallback((user) => {
    setUsers(prev => [...prev, user]);
  }, []);

  //  Good - function with dependency
  const removeUser = useCallback((userId) => {
    setUsers(prev => prev.filter(user => user.id !== userId));
  }, []);

  // Bad - expensive calculation on every render
  const badFilteredUsers = users.filter(user =>
    user.name.toLowerCase().includes(filter.toLowerCase())
  ).sort((a, b) => a[sortBy].localeCompare(b[sortBy]));

  //  Good - memoized expensive calculation
  const filteredUsers = useMemo(() => {
    console.log('Filtering and sorting users...');
    return users
      .filter(user =>
        user.name.toLowerCase().includes(filter.toLowerCase())
      )
      .sort((a, b) => a[sortBy].localeCompare(b[sortBy]));
  }, [users, filter, sortBy]);

  // Good - memoized derived data
  const userStats = useMemo(() => {
    return {
      total: users.length,
      filtered: filteredUsers.length,
      active: users.filter(user => user.active).length
    };
  }, [users, filteredUsers]);

  return (
    <div>
      <SearchFilter 
        filter={filter}
        onFilterChange={setFilter}
        sortBy={sortBy}
        onSortChange={setSortBy}
      />
      
      <UserStats stats={userStats} />
      
      <UserList
        users={filteredUsers}
        onAddUser={addUser}
        onRemoveUser={removeUser}
      />
    </div>
  );
}

// Optimized child components
const SearchFilter = memo(({ filter, onFilterChange, sortBy, onSortChange }) => {
  console.log('SearchFilter rendered');
  
  return (
    <div className="search-filter">
      <input
        type="text"
        value={filter}
        onChange={(e) => onFilterChange(e.target.value)}
        placeholder="Filter users..."
      />
      <select value={sortBy} onChange={(e) => onSortChange(e.target.value)}>
        <option value="name">Name</option>
        <option value="email">Email</option>
        <option value="date">Date</option>
      </select>
    </div>
  );
});

const UserStats = memo(({ stats }) => {
  console.log('UserStats rendered');
  
  return (
    <div className="user-stats">
      <div>Total: {stats.total}</div>
      <div>Filtered: {stats.filtered}</div>
      <div>Active: {stats.active}</div>
    </div>
  );
});

const UserList = memo(({ users, onAddUser, onRemoveUser }) => {
  console.log('UserList rendered');
  
  return (
    <div className="user-list">
      {users.map(user => (
        <UserItem
          key={user.id}
          user={user}
          onRemove={onRemoveUser}
        />
      ))}
    </div>
  );
});

const UserItem = memo(({ user, onRemove }) => {
  console.log(`UserItem ${user.id} rendered`);
  
  return (
    <div className="user-item">
      <span>{user.name} - {user.email}</span>
      <button onClick={() => onRemove(user.id)}>Remove</button>
    </div>
  );
});

Context Optimization

jsx

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

// Bad context - causes unnecessary re-renders
const BadAppContext = createContext();

function BadAppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState([]);

  // This value changes on every render
  const value = {
    user,
    setUser,
    theme,
    setTheme,
    notifications,
    setNotifications
  };

  return (
    <BadAppContext.Provider value={value}>
      {children}
    </BadAppContext.Provider>
  );
}

// Optimized context
const AppContext = createContext();

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState([]);

  // Memoize the context value
  const value = useMemo(() => ({
    user,
    setUser,
    theme,
    setTheme,
    notifications,
    setNotifications
  }), [user, theme, notifications]);

  return (
    <AppContext.Provider value={value}>
      {children}
    </AppContext.Provider>
  );
}

// Split contexts for better performance
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationContext = createContext();

function SplitContextProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState([]);

  return (
    <UserContext.Provider value={useMemo(() => [user, setUser], [user])}>
      <ThemeContext.Provider value={useMemo(() => [theme, setTheme], [theme])}>
        <NotificationContext.Provider value={useMemo(() => [notifications, setNotifications], [notifications])}>
          {children}
        </NotificationContext.Provider>
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

// Optimized consumers
function UserProfile() {
  const [user] = useContext(UserContext);
  // Only re-renders when user changes
  
  return <div>Welcome, {user?.name}</div>;
}

function ThemeToggle() {
  const [theme, setTheme] = useContext(ThemeContext);
  // Only re-renders when theme changes
  
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Current: {theme}
    </button>
  );
}

4. Virtualization with react-window

Basic Virtualized List

jsx

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

// Without virtualization - renders ALL items
function RegularList({ items }) {
  return (
    <div className="list" style={{ height: '400px', overflow: 'auto' }}>
      {items.map(item => (
        <ListItem key={item.id} item={item} />
      ))}
    </div>
  );
}

// With virtualization - renders only visible items
function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      <ListItem item={items[index]} />
    </div>
  );

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

const ListItem = memo(({ item }) => {
  return (
    <div className="list-item">
      <img src={item.avatar} alt={item.name} width="50" height="50" />
      <div>
        <h3>{item.name}</h3>
        <p>{item.email}</p>
        <p>{item.description}</p>
      </div>
    </div>
  );
});

Advanced Virtualization Examples

jsx

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

// Variable size list for dynamic content
function VariableSizeVirtualizedList({ items }) {
  const getItemSize = index => {
    const item = items[index];
    // Calculate height based on content
    const baseHeight = 80;
    const descriptionHeight = Math.ceil(item.description.length / 100) * 20;
    return baseHeight + descriptionHeight;
  };

  const Row = ({ index, style }) => (
    <div style={style}>
      <VariableListItem item={items[index]} />
    </div>
  );

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

// Virtualized grid
function VirtualizedGrid({ items, columns = 4 }) {
  const rows = useMemo(() => {
    const result = [];
    for (let i = 0; i < items.length; i += columns) {
      result.push(items.slice(i, i + columns));
    }
    return result;
  }, [items, columns]);

  const Row = ({ index, style }) => (
    <div style={style} className="grid-row">
      {rows[index].map(item => (
        <GridItem key={item.id} item={item} />
      ))}
    </div>
  );

  return (
    <List
      height={400}
      itemCount={rows.length}
      itemSize={120} // Height of each grid row
      width="100%"
    >
      {Row}
    </List>
  );
}

// Virtualized table with headers
function VirtualizedTable({ columns, data }) {
  const Row = ({ index, style }) => {
    const row = data[index];
    return (
      <div style={style} className="table-row">
        {columns.map(column => (
          <div key={column.key} className="table-cell">
            {row[column.key]}
          </div>
        ))}
      </div>
    );
  };

  return (
    <div className="virtualized-table">
      {/* Fixed header */}
      <div className="table-header">
        {columns.map(column => (
          <div key={column.key} className="header-cell">
            {column.title}
          </div>
        ))}
      </div>
      
      {/* Virtualized body */}
      <List
        height={300}
        itemCount={data.length}
        itemSize={40}
        width="100%"
      >
        {Row}
      </List>
    </div>
  );
}

// Infinite loading virtualized list
function InfiniteVirtualizedList({ items, hasMore, loadMore, loading }) {
  const itemCount = hasMore ? items.length + 1 : items.length;

  const Row = ({ index, style }) => {
    if (index === items.length) {
      return (
        <div style={style} className="loading-row">
          {loading ? 'Loading more...' : 'No more items'}
        </div>
      );
    }

    return (
      <div style={style}>
        <ListItem item={items[index]} />
      </div>
    );
  };

  const loadMoreItems = () => {
    if (hasMore && !loading) {
      loadMore();
    }
  };

  return (
    <List
      height={400}
      itemCount={itemCount}
      itemSize={80}
      width="100%"
      onItemsRendered={({ visibleStopIndex }) => {
        if (visibleStopIndex >= items.length - 5) {
          loadMoreItems();
        }
      }}
    >
      {Row}
    </List>
  );
}

Performance Monitoring and Debugging

jsx

import React, { Profiler } from 'react';

// Performance monitoring component
function PerformanceMonitor() {
  const onRender = (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
    console.log({
      id,
      phase,
      actualDuration,
      baseDuration,
      startTime,
      commitTime
    });
    
    // Log slow renders
    if (actualDuration > 16) { // 16ms = 60fps
      console.warn(`Slow render detected in ${id}: ${actualDuration}ms`);
    }
  };

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

// Custom hook for performance tracking
function useRenderTracking(componentName) {
  const renderCount = React.useRef(0);
  
  React.useEffect(() => {
    renderCount.current += 1;
    console.log(`${componentName} rendered: ${renderCount.current} times`);
  });
}

// Why did you render (for development)
// Add this in your components during development:
/*
MyComponent.whyDidYouRender = {
  logOnDifferentValues: true,
  customName: 'MyComponent'
};
*/

Performance Optimization Checklist

Do:

  • Use React.memo for expensive components

  • Use useCallback for function props

  • Use useMemo for expensive calculations

  • Implement code splitting with lazy loading

  • Virtualize long lists

  • Optimize context providers

  • Use proper dependency arrays

Don't:

  • Over-optimize simple components

  • Use memo for components that change often

  • Create new objects/functions in render without useMemo/useCallback

  • Ignore the bundle size impact

  • Virtualize very short lists

This comprehensive performance optimization guide covers the most effective strategies for making your React applications faster and more efficient.

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?