NetcaneTechnologies

React

React Performance Playbook: Memoization, Virtualization, and Renders

Concrete steps to reduce unnecessary renders and keep React UIs fast as data and components grow.

Yasir Haleem3 min read

React performance issues usually show up as sluggish lists, heavy forms, or janky interactions. The main levers are fewer renders, cheaper renders, and rendering less DOM. Here’s a playbook that works in production.

Measure before you optimize

Use React DevTools Profiler to record a typical interaction (e.g. typing in a filter, opening a list). See which components re-render and how long they take. Fix the slowest and most frequent first. Don’t memoize everything by default; it adds cognitive load and can hide real issues.

Memoization: when it helps

React.memo skips re-renders when props are referentially equal. It helps when a parent re-renders often but the child’s props are usually unchanged. Combine with stable callbacks (useCallback) and stable objects/arrays (useMemo) so that “unchanged” is actually true.

const ExpensiveRow = memo(function ExpensiveRow({ item, onSelect }) {
  // Heavy render
  return <tr onClick={() => onSelect(item.id)}>...</tr>;
});
 
function Table({ items, selectedId, onSelect }) {
  const handleSelect = useCallback((id) => {
    onSelect(id);
  }, [onSelect]);
  return (
    <table>
      {items.map((item) => (
        <ExpensiveRow
          key={item.id}
          item={item}
          onSelect={handleSelect}
        />
      ))}
    </table>
  );
}

If the parent creates a new onSelect or new item reference every time, memo is useless. Stabilize those first. Use memo for list items, heavy cells, or components that sit under frequently updating state.

Virtualization: long lists

For long lists (hundreds or thousands of rows), render only the visible window. Libraries like react-window or TanStack Virtual mount a small number of DOM nodes and recycle them as the user scrolls. That keeps layout and paint cost constant regardless of list length.

import { FixedSizeList } from "react-window";
 
function VirtualList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      <ExpensiveRow item={items[index]} />
    </div>
  );
  return (
    <FixedSizeList
      height={400}
      itemCount={items.length}
      itemSize={48}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

Virtualize any list that can grow large; it’s often the biggest win. Keep row components as light as possible so scrolling stays smooth.

Splitting and lazy loading

Code-split routes and heavy components with React.lazy and Suspense. That reduces initial JS and speeds up first load. Lazy load below-the-fold or modal content so the critical path stays small. Use dynamic imports for big charts, editors, or admin-only sections.

State shape and subscription

Lift state only as high as needed. If only a subtree needs an update, keep state there so the rest of the tree doesn’t re-render. With Zustand or Redux, use selectors that return primitives or stable references so components only re-render when their slice actually changes. Avoid storing derived data in state; compute it in render or with useMemo so you don’t invalidate unnecessarily.

Summary

Profile first, then reduce renders (memo + stable props), virtualize long lists, and split/lazy load heavy code. Apply memoization where you’ve measured re-renders; use virtualization for any list that can get long. This playbook keeps React UIs responsive as they grow.

About the author

Yasir Haleem is founder and lead engineer at Netcane Technologies. He builds production Next.js sites with headless CMS platforms — Strapi, Contentful, Sanity, and WordPress — with a focus on performance, SEO, and maintainable architecture.

Let's work together

Tell us about your project. We respond within one business day with a clear scope, timeline, and estimate.