Skip to content
Home / Skills / React / Performance
RE

Performance

React advanced v1.0.0

React Performance Optimization

Overview

React performance optimization involves minimizing unnecessary re-renders, efficient data handling, and proper use of memoization. This skill covers profiling, React.memo, useMemo, useCallback, virtualization, and code splitting.


Key Concepts

React Rendering Process

┌─────────────────────────────────────────────────────────────┐
│                  React Render Cycle                          │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  Trigger ──▶ Render ──▶ Reconciliation ──▶ Commit ──▶ Paint │
│                                                              │
│  ┌────────────────────────────────────────────────────────┐ │
│  │ Trigger Causes:                                        │ │
│  │ • State change (setState)                              │ │
│  │ • Props change (parent re-render)                      │ │
│  │ • Context change                                       │ │
│  │ • Force update                                         │ │
│  └────────────────────────────────────────────────────────┘ │
│                                                              │
│  Optimization Strategies:                                    │
│  ┌────────────────────────────────────────────────────────┐ │
│  │                                                        │ │
│  │  Reduce Renders:                                       │ │
│  │  ├── React.memo for pure components                   │ │
│  │  ├── useMemo for expensive computations               │ │
│  │  ├── useCallback for stable function references       │ │
│  │  └── Context splitting to reduce subscribers          │ │
│  │                                                        │ │
│  │  Reduce Work per Render:                              │ │
│  │  ├── Virtualize long lists                            │ │
│  │  ├── Lazy load components                             │ │
│  │  ├── Defer non-critical updates                       │ │
│  │  └── Use CSS instead of JS for animations            │ │
│  │                                                        │ │
│  │  Reduce Bundle Size:                                  │ │
│  │  ├── Code splitting                                   │ │
│  │  ├── Tree shaking                                     │ │
│  │  └── Dynamic imports                                  │ │
│  │                                                        │ │
│  └────────────────────────────────────────────────────────┘ │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Best Practices

1. Profile Before Optimizing

Use React DevTools Profiler to identify actual bottlenecks.

2. Don’t Memoize Everything

Memoization has overhead; only memoize when needed.

3. Keep Component Trees Shallow

Deep nesting increases reconciliation work.

4. Use Stable References

Prevent unnecessary re-renders with stable props.

5. Virtualize Large Lists

Only render visible items.


Code Examples

Example 1: React.memo with Custom Comparison

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

// Heavy component that should only re-render when needed
interface UserCardProps {
  user: User;
  onSelect: (id: string) => void;
  isSelected: boolean;
}

// Without memo - re-renders whenever parent re-renders
function UserCard({ user, onSelect, isSelected }: UserCardProps) {
  console.log(`Rendering UserCard for ${user.id}`);
  
  return (
    <div 
      className={`user-card ${isSelected ? 'selected' : ''}`}
      onClick={() => onSelect(user.id)}
    >
      <img src={user.avatar} alt={user.name} />
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  );
}

// With memo - only re-renders when props actually change
const MemoizedUserCard = memo(UserCard);

// Custom comparison for complex props
const MemoizedWithCustomCompare = memo(
  UserCard,
  (prevProps, nextProps) => {
    // Return true if props are equal (skip re-render)
    return (
      prevProps.user.id === nextProps.user.id &&
      prevProps.user.updatedAt === nextProps.user.updatedAt &&
      prevProps.isSelected === nextProps.isSelected &&
      prevProps.onSelect === nextProps.onSelect
    );
  }
);

// Parent component
function UserList({ users }: { users: User[] }) {
  const [selectedId, setSelectedId] = useState<string | null>(null);

  // ❌ BAD: Creates new function on every render
  // const handleSelect = (id: string) => setSelectedId(id);

  // ✅ GOOD: Stable function reference
  const handleSelect = useCallback((id: string) => {
    setSelectedId(id);
  }, []);

  return (
    <div className="user-list">
      {users.map((user) => (
        <MemoizedUserCard
          key={user.id}
          user={user}
          onSelect={handleSelect}
          isSelected={user.id === selectedId}
        />
      ))}
    </div>
  );
}

Example 2: useMemo and useCallback

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

interface DataTableProps {
  data: Item[];
  filters: Filters;
  sortConfig: SortConfig;
}

function DataTable({ data, filters, sortConfig }: DataTableProps) {
  const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());

  // ✅ Expensive computation - memoize the result
  const processedData = useMemo(() => {
    console.log('Processing data...');
    
    // Filter
    let result = data.filter((item) => {
      if (filters.search && !item.name.includes(filters.search)) {
        return false;
      }
      if (filters.category && item.category !== filters.category) {
        return false;
      }
      return true;
    });

    // Sort
    result = [...result].sort((a, b) => {
      const aVal = a[sortConfig.key];
      const bVal = b[sortConfig.key];
      const direction = sortConfig.direction === 'asc' ? 1 : -1;
      
      if (typeof aVal === 'string') {
        return aVal.localeCompare(bVal as string) * direction;
      }
      return ((aVal as number) - (bVal as number)) * direction;
    });

    return result;
  }, [data, filters, sortConfig]); // Only recompute when these change

  // ✅ Stable callback - prevents child re-renders
  const toggleRow = useCallback((id: string) => {
    setExpandedRows((prev) => {
      const next = new Set(prev);
      if (next.has(id)) {
        next.delete(id);
      } else {
        next.add(id);
      }
      return next;
    });
  }, []);

  // ✅ Derived state - simple calculation, no need for useMemo
  const totalValue = processedData.reduce((sum, item) => sum + item.value, 0);

  // ❌ DON'T memoize simple values
  // const isFiltered = useMemo(() => !!filters.search, [filters.search]);
  
  // ✅ Just compute it
  const isFiltered = !!filters.search || !!filters.category;

  return (
    <table>
      <tbody>
        {processedData.map((item) => (
          <TableRow
            key={item.id}
            item={item}
            isExpanded={expandedRows.has(item.id)}
            onToggle={toggleRow}
          />
        ))}
      </tbody>
      <tfoot>
        <tr>
          <td>Total: {totalValue}</td>
        </tr>
      </tfoot>
    </table>
  );
}

// Memoized row component
const TableRow = memo(function TableRow({
  item,
  isExpanded,
  onToggle,
}: {
  item: Item;
  isExpanded: boolean;
  onToggle: (id: string) => void;
}) {
  return (
    <>
      <tr onClick={() => onToggle(item.id)}>
        <td>{item.name}</td>
        <td>{item.value}</td>
      </tr>
      {isExpanded && (
        <tr>
          <td colSpan={2}>{item.details}</td>
        </tr>
      )}
    </>
  );
});

Example 3: Virtual List

import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef, useMemo } from 'react';

interface VirtualListProps {
  items: Item[];
  itemHeight: number;
}

function VirtualList({ items, itemHeight }: VirtualListProps) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => itemHeight,
    overscan: 5, // Render 5 extra items above/below viewport
  });

  const virtualItems = virtualizer.getVirtualItems();

  return (
    <div
      ref={parentRef}
      style={{
        height: '400px',
        overflow: 'auto',
      }}
    >
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          width: '100%',
          position: 'relative',
        }}
      >
        {virtualItems.map((virtualItem) => {
          const item = items[virtualItem.index];
          return (
            <div
              key={virtualItem.key}
              style={{
                position: 'absolute',
                top: 0,
                left: 0,
                width: '100%',
                height: `${virtualItem.size}px`,
                transform: `translateY(${virtualItem.start}px)`,
              }}
            >
              <ItemRow item={item} />
            </div>
          );
        })}
      </div>
    </div>
  );
}

// Virtual grid for 2D layouts
function VirtualGrid({ items, columns }: { items: Item[]; columns: number }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const rowCount = Math.ceil(items.length / columns);

  const rowVirtualizer = useVirtualizer({
    count: rowCount,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 200,
    overscan: 2,
  });

  const columnVirtualizer = useVirtualizer({
    horizontal: true,
    count: columns,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 200,
    overscan: 2,
  });

  return (
    <div
      ref={parentRef}
      style={{
        height: '600px',
        width: '100%',
        overflow: 'auto',
      }}
    >
      <div
        style={{
          height: `${rowVirtualizer.getTotalSize()}px`,
          width: `${columnVirtualizer.getTotalSize()}px`,
          position: 'relative',
        }}
      >
        {rowVirtualizer.getVirtualItems().map((virtualRow) =>
          columnVirtualizer.getVirtualItems().map((virtualColumn) => {
            const index = virtualRow.index * columns + virtualColumn.index;
            const item = items[index];
            
            if (!item) return null;

            return (
              <div
                key={`${virtualRow.index}-${virtualColumn.index}`}
                style={{
                  position: 'absolute',
                  top: 0,
                  left: 0,
                  width: `${virtualColumn.size}px`,
                  height: `${virtualRow.size}px`,
                  transform: `translateX(${virtualColumn.start}px) translateY(${virtualRow.start}px)`,
                }}
              >
                <GridItem item={item} />
              </div>
            );
          })
        )}
      </div>
    </div>
  );
}

Example 4: Code Splitting and Lazy Loading

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

// Lazy load heavy components
const HeavyChart = lazy(() => import('./components/HeavyChart'));
const DataTable = lazy(() => import('./components/DataTable'));
const ReportGenerator = lazy(() => 
  import('./components/ReportGenerator').then(module => ({
    default: module.ReportGenerator
  }))
);

// Preload on hover
const preloadChart = () => {
  import('./components/HeavyChart');
};

// Route-based code splitting
const routes = {
  dashboard: lazy(() => import('./pages/Dashboard')),
  analytics: lazy(() => import('./pages/Analytics')),
  settings: lazy(() => import('./pages/Settings')),
};

function App() {
  const [activeTab, setActiveTab] = useState('overview');
  const [isPending, startTransition] = useTransition();

  const handleTabChange = (tab: string) => {
    // Use transition for non-urgent updates
    startTransition(() => {
      setActiveTab(tab);
    });
  };

  return (
    <div className="app">
      <nav>
        <button
          onClick={() => handleTabChange('overview')}
          onMouseEnter={preloadChart}
        >
          Overview
        </button>
        <button onClick={() => handleTabChange('details')}>
          Details
        </button>
        <button onClick={() => handleTabChange('reports')}>
          Reports
        </button>
      </nav>

      {isPending && <LoadingBar />}

      <Suspense fallback={<FullPageSpinner />}>
        {activeTab === 'overview' && <HeavyChart />}
        {activeTab === 'details' && <DataTable />}
        {activeTab === 'reports' && <ReportGenerator />}
      </Suspense>
    </div>
  );
}

// Nested suspense for progressive loading
function Dashboard() {
  return (
    <div className="dashboard">
      {/* Critical content loads first */}
      <Suspense fallback={<HeaderSkeleton />}>
        <DashboardHeader />
      </Suspense>

      <div className="dashboard-grid">
        {/* Non-critical content can load independently */}
        <Suspense fallback={<CardSkeleton />}>
          <RevenueCard />
        </Suspense>

        <Suspense fallback={<CardSkeleton />}>
          <UsersCard />
        </Suspense>

        <Suspense fallback={<ChartSkeleton />}>
          <ActivityChart />
        </Suspense>
      </div>
    </div>
  );
}

Example 5: Avoiding Common Performance Pitfalls

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

// ❌ BAD: Inline object creates new reference every render
function BadComponent() {
  return <Child style={{ color: 'red' }} />;
}

// ✅ GOOD: Stable reference
const childStyle = { color: 'red' };
function GoodComponent() {
  return <Child style={childStyle} />;
}

// ❌ BAD: Inline function creates new reference
function BadHandler() {
  return <Button onClick={() => console.log('clicked')} />;
}

// ✅ GOOD: useCallback for stable reference
function GoodHandler() {
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);
  return <Button onClick={handleClick} />;
}

// ❌ BAD: Spreading state causes all children to re-render
function BadContext() {
  const [state, setState] = useState({ user: null, theme: 'dark', locale: 'en' });
  return (
    <AppContext.Provider value={{ ...state, setState }}>
      <App />
    </AppContext.Provider>
  );
}

// ✅ GOOD: Split contexts by update frequency
function GoodContext() {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('dark');
  
  return (
    <UserContext.Provider value={{ user, setUser }}>
      <ThemeContext.Provider value={{ theme, setTheme }}>
        <App />
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

// ❌ BAD: Computing in render without memoization
function BadComputation({ items }: { items: Item[] }) {
  // Runs on every render
  const sorted = items.slice().sort((a, b) => a.name.localeCompare(b.name));
  return <List items={sorted} />;
}

// ✅ GOOD: Memoized computation
function GoodComputation({ items }: { items: Item[] }) {
  const sorted = useMemo(
    () => items.slice().sort((a, b) => a.name.localeCompare(b.name)),
    [items]
  );
  return <List items={sorted} />;
}

// Using refs for values that don't need re-renders
function FormWithRef() {
  // ❌ Causes re-render on every keystroke
  // const [value, setValue] = useState('');
  
  // ✅ No re-renders, access via ref.current
  const inputRef = useRef<HTMLInputElement>(null);
  
  const handleSubmit = () => {
    const value = inputRef.current?.value;
    submitForm(value);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input ref={inputRef} />
      <button type="submit">Submit</button>
    </form>
  );
}

Anti-Patterns

❌ Premature Optimization

// WRONG - memoizing everything without profiling
const Component = memo(({ title }) => <h1>{title}</h1>);
const memoizedTitle = useMemo(() => title.toUpperCase(), [title]);

// ✅ CORRECT - profile first, then optimize bottlenecks
function Component({ title }) {
  return <h1>{title}</h1>;
}

❌ Incorrect Dependency Arrays

// WRONG - missing dependency causes stale closure
const handleClick = useCallback(() => {
  console.log(count); // Stale!
}, []); // Missing count

// ✅ CORRECT
const handleClick = useCallback(() => {
  console.log(count);
}, [count]);

References