React Performance Patterns: From Memo to Virtualization
ReactTypeScriptCoding

React Performance Patterns: From Memo to Virtualization

Ihor ChyshkalaIhor Chyshkala

Performance optimization in React applications requires a deep understanding of rendering behavior and various optimization techniques. In this article, we'll explore different performance patterns, from basic memoization to advanced virtualization techniques.

Understanding React's Rendering Behavior

Before diving into optimization patterns, let's understand when and why React components re-render:

function ParentComponent() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <Counter count={count} />
      <ExpensiveComponent /> {/* Re-renders on every count change */}
    </div>
  );
}

function ExpensiveComponent() {
  // Expensive calculations or rendering
  return <div>{/* ... */}</div>;
}

Memoization Patterns

Using React.memo

// Before optimization
function MovieCard({ title, rating, onFavorite }: MovieCardProps) {
  return (
    <div className="card">
      <h3>{title}</h3>
      <div>{rating} stars</div>
      <button onClick={onFavorite}>Favorite</button>
    </div>
  );
}

// After optimization
const MovieCard = React.memo(
  function MovieCard({ title, rating, onFavorite }: MovieCardProps) {
    return (
      <div className="card">
        <h3>{title}</h3>
        <div>{rating} stars</div>
        <button onClick={onFavorite}>Favorite</button>
      </div>
    );
  },
  (prevProps, nextProps) => {
    return (
      prevProps.title === nextProps.title &&
      prevProps.rating === nextProps.rating
    );
  }
);

Optimizing with useMemo

function SearchResults({ items, query }: SearchResultsProps) {
  const filteredItems = useMemo(() => {
    console.log('Filtering items...'); // Expensive operation
    return items.filter(item =>
      item.title.toLowerCase().includes(query.toLowerCase())
    );
  }, [items, query]);

  return (
    <ul>
      {filteredItems.map(item => (
        <li key={item.id}>{item.title}</li>
      ))}
    </ul>
  );
}

useCallback for Event Handlers

function ProductList({ products }: ProductListProps) {
  const [favorites, setFavorites] = useState<Set<string>>(new Set());

  const handleFavorite = useCallback((productId: string) => {
    setFavorites(prev => {
      const next = new Set(prev);
      if (next.has(productId)) {
        next.delete(productId);
      } else {
        next.add(productId);
      }
      return next;
    });
  }, []);

  return (
    <div>
      {products.map(product => (
        <MovieCard
          key={product.id}
          {...product}
          onFavorite={() => handleFavorite(product.id)}
        />
      ))}
    </div>
  );
}

Debouncing and Throttling

Custom Debounce Hook

function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);

  return debouncedValue;
}

// Usage in search component
function SearchComponent() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearch = useDebounce(searchTerm, 300);

  useEffect(() => {
    // API call here
    console.log('Searching for:', debouncedSearch);
  }, [debouncedSearch]);

  return (
    <input
      type="text"
      value={searchTerm}
      onChange={(e) => setSearchTerm(e.target.value)}
    />
  );
}

Custom Throttle Hook

function useThrottle<T>(value: T, interval: number): T {
  const [throttledValue, setThrottledValue] = useState(value);
  const lastExecuted = useRef<number>(Date.now());

  useEffect(() => {
    const handler = setTimeout(() => {
      const now = Date.now();
      if (now >= lastExecuted.current + interval) {
        setThrottledValue(value);
        lastExecuted.current = now;
      }
    }, interval);

    return () => {
      clearTimeout(handler);
    };
  }, [value, interval]);

  return throttledValue;
}

Virtualization Techniques

Basic Virtualization Hook

interface UseVirtualizationProps {
  itemCount: number;
  itemHeight: number;
  containerHeight: number;
  overscan?: number;
}

function useVirtualization({
  itemCount,
  itemHeight,
  containerHeight,
  overscan = 3
}: UseVirtualizationProps) {
  const [scrollTop, setScrollTop] = useState(0);

  const visibleItems = useMemo(() => {
    const startIndex = Math.max(
      0,
      Math.floor(scrollTop / itemHeight) - overscan
    );
    const endIndex = Math.min(
      itemCount - 1,
      Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
    );

    return {
      startIndex,
      endIndex,
      items: Array.from(
        { length: endIndex - startIndex + 1 },
        (_, index) => startIndex + index
      ),
      offsetY: startIndex * itemHeight
    };
  }, [scrollTop, itemHeight, containerHeight, itemCount, overscan]);

  return {
    visibleItems,
    onScroll: (e: React.UIEvent<HTMLDivElement>) => {
      setScrollTop(e.currentTarget.scrollTop);
    }
  };
}

// Usage
function VirtualizedList({ items }: { items: string[] }) {
  const {
    visibleItems,
    onScroll
  } = useVirtualization({
    itemCount: items.length,
    itemHeight: 40,
    containerHeight: 400
  });

  return (
    <div
      style={{ height: '400px', overflow: 'auto' }}
      onScroll={onScroll}
    >
      <div style={{ height: `${items.length * 40}px`, position: 'relative' }}>
        {visibleItems.items.map(index => (
          <div
            key={index}
            style={{
              position: 'absolute',
              top: `${index * 40}px`,
              height: '40px',
              width: '100%'
            }}
          >
            {items[index]}
          </div>
        ))}
      </div>
    </div>
  );
}

Code Splitting and Lazy Loading

Route-Based Code Splitting

import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/profile" element={<Profile />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

Component-Based Code Splitting

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

function Dashboard() {
  const [showChart, setShowChart] = useState(false);

  return (
    <div>
      <button onClick={() => setShowChart(true)}>Show Chart</button>
      {showChart && (
        <Suspense fallback={<LoadingSpinner />}>
          <HeavyChart />
        </Suspense>
      )}
    </div>
  );
}

Performance Monitoring

Custom Performance Hook

function usePerformanceMonitor(componentName: string) {
  const renderCount = useRef(0);
  const lastRenderTime = useRef(performance.now());

  useEffect(() => {
    const renderTime = performance.now() - lastRenderTime.current;
    renderCount.current += 1;

    console.log(`${componentName} render #${renderCount.current} took ${renderTime}ms`);
    
    lastRenderTime.current = performance.now();
  });
}

// Usage
function ExpensiveComponent() {
  usePerformanceMonitor('ExpensiveComponent');
  // ... component logic
}

Measuring Re-Renders

Best Practices and Guidelines

  1. When to Use Memoization
    • For expensive calculations
    • For preventing unnecessary re-renders
    • When props are stable
// Good use case
const expensiveValue = useMemo(() => {
  return someExpensiveCalculation(props.data);
}, [props.data]);

// Bad use case (over-optimization)
const simpleValue = useMemo(() => {
  return props.value + 1;
}, [props.value]);

2. Virtualization Considerations

  • Use for large lists (100+ items)
  • Consider variable height items
  • Handle scroll restoration

3. Code Splitting Guidelines

  • Split by route
  • Split by feature
  • Split by viewport visibility

4. Performance Testing

describe('Performance Tests', () => {
  it('should render list efficiently', async () => {
    const startTime = performance.now();
    
    render(<VirtualizedList items={largeDataSet} />);
    
    const endTime = performance.now();
    expect(endTime - startTime).toBeLessThan(100);
  });
});

Conclusion

React performance optimization is a balance between:

  • Code complexity
  • Bundle size
  • Runtime performance
  • Development experience

Key takeaways:

  1. Use memoization judiciously
  2. Implement virtualization for large lists
  3. Split code based on user needs
  4. Monitor and measure performance
  5. Focus on user-perceived performance

About the Author

Ihor Chyshkala

Ihor Chyshkala

Code Alchemist: Transmuting Ideas into Reality with JS & PHP. DevOps Wizard: Transforming Infrastructure into Cloud Gold | Orchestrating CI/CD Magic | Crafting Automation Elixirs