SKIP TO CONTENT
HOME/LEARN/FRONTEND
REACTPERFORMANCE

REACT PERFORMANCE PATTERNS

Memoization, virtualization, code splitting, and avoiding unnecessary re-renders in large-scale React apps.

Why Performance Matters

In production apps with complex dashboards, forms, and data tables, even small inefficiencies compound. A single unnecessary re-render in a parent component can cascade through hundreds of children. Performance isn't about premature optimization — it's about building the right mental model from the start.

Memoization — React.memo, useMemo, useCallback

React.memo prevents re-renders when props haven't changed. useMemo caches expensive computations. useCallback stabilizes function references. The key insight: don't memoize everything — profile first, then memoize the hot paths.

tsx
// Memoize an expensive config diff computation
const hasChanges = useMemo(() => {
  return deepDiff(currentConfig, savedConfig);
}, [currentConfig, savedConfig]);

// Stabilize a callback passed to memoized children
const handleSelect = useCallback((id: string) => {
  dispatch(selectItem(id));
}, [dispatch]);

// Prevent re-renders when props are the same
const DataRow = React.memo(({ row }: { row: Row }) => {
  return <tr>{row.cells.map(cell => <td key={cell.id}>{cell.value}</td>)}</tr>;
});

Virtualization for Large Lists

Rendering 10K+ rows in a data table? The DOM can't handle it. Virtualization renders only visible rows. Combined with sticky headers and dynamic row heights, you get buttery scrolling even with massive datasets.

tsx
import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualTable({ rows }: { rows: Data[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: rows.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 48, // estimated row height
    overscan: 10,           // render 10 extra rows outside viewport
  });

  return (
    <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize() }}>
        {virtualizer.getVirtualItems().map(virtualRow => (
          <div
            key={virtualRow.key}
            style={{
              position: 'absolute',
              top: virtualRow.start,
              height: virtualRow.size,
            }}
          >
            <DataRow row={rows[virtualRow.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}

Code Splitting & Lazy Loading

Use React.lazy() and dynamic imports to split your bundle by route and by feature. Heavy components like chart libraries and editors should always be lazy loaded.

tsx
// Next.js dynamic import — client-only heavy component
import dynamic from 'next/dynamic';

const VisualEditor = dynamic(() => import('./VisualEditor'), {
  ssr: false,
  loading: () => <EditorSkeleton />,
});

// Route-level code splitting
const DashboardPage = lazy(() => import('./pages/Dashboard'));
const SettingsPage = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<PageLoader />}>
      <Routes>
        <Route path="/dashboard" element={<DashboardPage />} />
        <Route path="/settings" element={<SettingsPage />} />
      </Routes>
    </Suspense>
  );
}

Avoiding Re-Render Cascades

Common culprits: creating new objects/arrays in render, passing anonymous functions as props, and using Context for frequently-changing values. Solutions: lift state down, split contexts, and use refs for non-render values.

tsx
// BAD: new object every render → children re-render
<List style={{ marginTop: 10 }} />

// GOOD: stable reference
const listStyle = useMemo(() => ({ marginTop: 10 }), []);
<List style={listStyle} />

// Split contexts by update frequency
const LayoutConfigContext = createContext(null);  // rare updates
const SelectionContext = createContext(null);      // frequent updates
const UIPanelContext = createContext(null);         // medium updates

// Use ref for values that don't need renders
const dragStateRef = useRef({ isDragging: false, startX: 0, startY: 0 });