React Performance Optimization: Beyond the Basics

React Performance Optimization: Beyond the Basics


React is fast by default, but as applications grow in complexity, performance can become a concern. Here are some advanced optimization techniques I’ve used in production applications.

Component-Level Optimizations

Memoization with React.memo

React.memo is great for preventing unnecessary re-renders, but it’s not a silver bullet. Use it judiciously:

import { memo } from 'react';

const ExpensiveComponent = memo(({ data, onUpdate }) => {
  // Complex rendering logic
  return <div>{/* render data */}</div>;
}, (prevProps, nextProps) => {
  // Custom comparison function
  return prevProps.data.id === nextProps.data.id;
});

useMemo and useCallback

These hooks prevent expensive calculations and function recreations:

import { useMemo, useCallback } from 'react';

function DataTable({ data, filters }) {
  // Expensive filtering operation
  const filteredData = useMemo(() => {
    return data.filter(item =>
      filters.every(filter => filter(item))
    );
  }, [data, filters]);

  // Prevent function recreation
  const handleSort = useCallback((column) => {
    // sorting logic
  }, []);

  return <Table data={filteredData} onSort={handleSort} />;
}

Virtual Scrolling for Large Lists

When rendering thousands of items, virtual scrolling is essential. Libraries like react-window only render visible items:

import { FixedSizeList } from 'react-window';

function VirtualList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>{items[index].name}</div>
  );

  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={35}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

Code Splitting and Lazy Loading

Break your bundle into smaller chunks that load on demand:

import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <HeavyComponent />
    </Suspense>
  );
}

State Management Optimization

Avoid Unnecessary Context Updates

Split contexts to prevent unnecessary re-renders:

// Instead of one large context
const UserContext = createContext();

// Split into focused contexts
const UserDataContext = createContext();
const UserActionsContext = createContext();

Use State Batching

React 18’s automatic batching improves performance:

// These updates are automatically batched in React 18
function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // Only one re-render occurs
}

Production Build Optimizations

  1. Enable Production Mode: Ensures React runs optimized code
  2. Tree Shaking: Remove unused code from bundles
  3. Compression: Enable gzip/brotli compression
  4. CDN: Serve static assets from a CDN

Measuring Performance

Use React DevTools Profiler to identify bottlenecks:

import { Profiler } from 'react';

function onRenderCallback(
  id,
  phase,
  actualDuration,
  baseDuration,
  startTime,
  commitTime
) {
  console.log(`${id} (${phase}) took ${actualDuration}ms`);
}

<Profiler id="App" onRender={onRenderCallback}>
  <App />
</Profiler>

Conclusion

Performance optimization is an iterative process. Profile first, optimize what matters, and don’t prematurely optimize. These techniques have helped me ship faster React applications without sacrificing developer experience.