Skip to content
Web Development

React Performance: 8 Practical Ways to Build Faster Interfaces

Measure first, then fix the real bottleneck. Eight techniques — from avoiding needless re-renders to virtualising long lists — with code you can use today.

Priya Mehta4 min read
React component code using memo, useMemo and a virtualised list beside an atom illustration and performance metrics
On this page

Most React apps are fast enough most of the time. When they are not, the cause is usually one of a handful of patterns: too much work during render, too many components re-rendering, too much JavaScript shipped, or too many DOM nodes. This guide covers eight practical fixes, but the first rule matters more than any of them: measure before you optimise.

1. Measure with the right tools

Use the React Developer Tools Profiler to record an interaction and see which components rendered and how long they took. Enable “Highlight updates when components render” to spot unexpected re-renders. For user-facing metrics, check Core Web Vitals — especially Interaction to Next Paint (INP) for responsiveness and Largest Contentful Paint (LCP) for loading — using Lighthouse in development and field data from real users where available.

Write down a baseline number. Without one, you cannot tell whether a change helped.

2. Keep state close to where it is used

When state lives high in the tree, every update re-renders everything below it. Moving state down is often the single most effective optimisation, and it requires no memoisation at all.

// Before: typing in the search box re-renders the whole page.
function Page() {
  const [query, setQuery] = useState("");
  return (
    <>
      <SearchBox value={query} onChange={setQuery} />
      <ExpensiveDashboard />
    </>
  );
}

// After: only SearchBox re-renders while typing.
function Page() {
  return (
    <>
      <SearchBox />
      <ExpensiveDashboard />
    </>
  );
}

3. Memoise expensive components with memo

React.memo skips re-rendering a component when its props are shallowly equal to last time. Use it on components that are expensive to render and receive stable props.

const ArticleRow = memo(function ArticleRow({ article, onSelect }) {
  return (
    <li onClick={() => onSelect(article.id)}>
      {article.title}
    </li>
  );
});

Memoisation only works if props are stable. Which leads to the next point.

4. Stabilise callbacks and derived values

Functions and objects created during render are new every time, so they break memo. useCallback and useMemo keep them stable between renders when their dependencies have not changed.

function ArticleList({ articles, query }) {
  const visible = useMemo(
    () => articles.filter((a) => a.title.toLowerCase().includes(query)),
    [articles, query]
  );
  const handleSelect = useCallback((id) => navigate(`/articles/${id}`), []);

  return visible.map((a) => (
    <ArticleRow key={a.id} article={a} onSelect={handleSelect} />
  ));
}

Do not wrap everything in useMemo. It has a cost too. Use it for genuinely expensive calculations and for values passed to memoised children. If your project uses the React Compiler, much of this is handled automatically — check your setup before adding manual memoisation.

5. Use stable, meaningful keys

Keys tell React which list item is which. Using the array index as a key causes unnecessary work — and subtle bugs with inputs — when items are inserted, removed or reordered. Use a stable ID from your data.

6. Virtualise long lists

Rendering 5,000 rows creates 5,000 sets of DOM nodes, even though only 20 are visible. Virtualisation renders only the rows in the viewport. Libraries such as TanStack Virtual or react-window handle the maths for you.

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

function VirtualList({ rows }) {
  const parentRef = useRef(null);
  const virtualizer = useVirtualizer({
    count: rows.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 56,
  });

  return (
    <div ref={parentRef} style={{ height: 480, overflow: "auto" }}>
      <div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
        {virtualizer.getVirtualItems().map((item) => (
          <div
            key={rows[item.index].id}
            style={{ position: "absolute", top: 0, transform: `translateY(${item.start}px)`, height: item.size }}
          >
            {rows[item.index].title}
          </div>
        ))}
      </div>
    </div>
  );
}

7. Keep typing responsive with transitions

Some updates are urgent (the characters appearing in an input) and some are not (filtering a large list). useTransition and useDeferredValue let React keep urgent updates responsive while rendering the expensive part in the background.

function Search({ items }) {
  const [query, setQuery] = useState("");
  const deferredQuery = useDeferredValue(query);
  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <Results items={items} query={deferredQuery} />
    </>
  );
}

This directly helps INP, because the browser can paint the next frame after a keystroke without waiting for the whole list.

8. Ship less JavaScript

The fastest code is code the browser never downloads.

  • Split routes and heavy widgets with lazy() and Suspense, so a charting library only loads on the page that uses it.
  • Check your bundle with an analyzer and replace large dependencies that you use for one function.
  • Optimise images: correct dimensions, modern formats and lazy loading below the fold.
const ReportsChart = lazy(() => import("./ReportsChart"));

function ReportsPage() {
  return (
    <Suspense fallback={<ChartSkeleton />}>
      <ReportsChart />
    </Suspense>
  );
}

A quick decision table

SymptomLikely fix
Whole page re-renders on each keystrokeMove state down, useDeferredValue
A heavy child re-renders with unchanged datamemo + stable props
Scrolling a long list stuttersVirtualisation
Slow first loadCode splitting, smaller bundle, image optimisation
Laggy clicks and inputs (poor INP)Transitions, less work per interaction

Performance work is iterative: measure, change one thing, measure again. Small, targeted fixes beat sweeping rewrites almost every time.

Written by

Priya Mehta

Frontend developer · React & performance

Focuses on fast, accessible interfaces and the JavaScript fundamentals behind them. (Demo author profile.)

Comments

Join the discussion

Your email address will not be published. Be kind and stay on topic.