Topic #42Core7 min read

Performance Optimization

Lazy-load routes, memoize components and callbacks, and virtualize long lists to keep React apps fast.

#performance#code-splitting#memoization#virtualization#react

Frontend performance comes down to shipping less JavaScript, doing less work on render, and rendering fewer DOM nodes. The four highest-leverage React techniques are code splitting (lazy-loading routes), React.memo (skipping re-renders when props are unchanged), useCallback (stabilizing function identity so memoized children don't re-render), and list virtualization (only rendering the rows currently on screen).

Lazy loading routes uses React.lazy + dynamic import() so each route becomes its own bundle chunk, loaded on demand and wrapped in a <Suspense> boundary with a fallback. This shrinks the initial bundle and speeds up first paint.

React.memo wraps a component so it re-renders only when its props change by shallow comparison. It pairs with useCallback, which memoizes a function so its identity stays stable across renders — without it, a freshly created handler passed as a prop would defeat React.memo on the child.

Virtualized lists (e.g. react-window) render only the visible window of a large list instead of thousands of DOM nodes. For 1000+ rows this is the difference between a janky and a smooth scroll. Reach for these tools when you measure a problem — premature memoization adds complexity without payoff.

Backend Analogy

Code splitting is lazy class loading / on-demand module initialization — you don't load every JAR into memory at boot, you load what a request actually needs. React.memo and useCallback are like a memoization cache or @Cacheable: skip recomputation when inputs are unchanged. List virtualization is pagination/streaming a result set instead of loading a million-row table into a List at once.

Key Insights
  • Code splitting with React.lazy + Suspense turns each route into its own chunk, cutting the initial bundle.
  • React.memo only helps if props are referentially stable — combine it with useCallback/useMemo for function and object props.
  • Virtualize lists past a few hundred rows; rendering only the visible window keeps the DOM node count flat regardless of data size.
  • Optimize what you measure: profile first, then memoize, because needless memoization adds memory and complexity for no gain.

Worked Code

1. Lazy loading routes (code splitting)
TSX
// 1. Lazy loading routes (code splitting)
import { lazy, Suspense } from 'react';

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

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/" element={<Dashboard />} />
        <Route path="/analytics" element={<Analytics />} />
      </Routes>
    </Suspense>
  );
}
2. React.memo — skip re-render if props unchanged
TSX
// 2. React.memo — skip re-render if props unchanged
const ExpenseCard = React.memo(function ExpenseCard({ expense }: Props) {
  return <div>{expense.category}: ₹{expense.amount}</div>;
});
3. useCallback — memoize functions passed as props
TSX
// 3. useCallback — memoize functions passed as props
function Parent() {
  const handleApprove = useCallback((id: number) => {
    dispatch(approveExpense(id));
  }, [dispatch]);

  return <ExpenseList onApprove={handleApprove} />;
}
4. Virtualized lists (for 1000+ items)
TSX
// 4. Virtualized lists (for 1000+ items)
import { FixedSizeList } from 'react-window';

function VirtualizedExpenses({ items }: { items: Expense[] }) {
  return (
    <FixedSizeList height={600} itemCount={items.length} itemSize={60} width="100%">
      {({ index, style }) => (
        <div style={style}>
          {items[index].category}: ₹{items[index].amount}
        </div>
      )}
    </FixedSizeList>
  );
}

Interview-Ready Q&A

Code splitting breaks the bundle into chunks loaded on demand, so the initial download and parse cost is smaller and first paint is faster. In React you use React.lazy with a dynamic import() for route or heavy components and wrap them in a <Suspense> boundary with a fallback. Each lazy import becomes its own chunk that the bundler loads only when the component is rendered.

Things to Remember
  • 1Four levers: lazy-load routes, React.memo, useCallback/useMemo, virtualize lists.
  • 2React.memo needs referentially stable props (useCallback/useMemo) to actually skip renders.
  • 3Measure before optimizing — premature memoization costs memory and clarity with no benefit.

References & Further Reading