Skip to content
elephantoo

Performance & React DevTools

Lesson 27 of 30 16 min read

Profile renders, fix slow components, virtualise long lists, split bundles and measure Web Vitals.


A fast app feels professional; a sluggish one drives users away. The good news is React apps are usually fast enough by default, and when they're not, the problem is almost always one of a handful of causes. This lesson shows you how to find problems with React DevTools and browser tools, and how to fix the common ones.

The golden rule: measure first#

Don't optimise based on hunches. The workflow is:

  1. Notice something slow (typing lags, a page takes ages to load).
  2. Measure with tools to find the actual cause.
  3. Fix that one thing.
  4. Measure again to confirm it helped.

Always measure a production build. Development mode is deliberately slower (extra checks, Strict Mode double-renders):

Terminal
npm run build
npm run preview   # serves dist/ at http://localhost:4173

React Developer Tools#

Install the React Developer Tools extension for Chrome, Firefox or Edge. It adds two tabs to your browser's DevTools.

⚛️ Components

  • Browse the component tree, just like the Elements panel shows the DOM.
  • Select a component to see and edit its props, state and hooks live.
  • See which component rendered a DOM element, and its owner chain ("rendered by").
  • Click the eye icon to inspect the matching DOM node; click <> to jump to the source.

In the settings (⚙️), turn on "Highlight updates when components render". Now every re-render flashes an outline on the page. Type into a search box: if the whole page flashes, you've found unnecessary re-renders.

⚛️ Profiler

  1. Open the Profiler tab and press Record (●).
  2. Perform the slow interaction (type, click, switch tabs).
  3. Press Stop.

You'll see each commit (a batch of updates applied to the DOM) as a bar. Select one to see:

  • Flame graph — the component tree for that commit; width and colour show render time. Grey components didn't render.
  • Ranked chart — components sorted by how long they took.
  • "Why did this render?" — enable "Record why each component rendered while profiling" in settings to see "Props changed: (onSelect)", "State changed", "Parent component rendered" and so on.

Typical findings: a component re-rendering because a parent passed a new inline function, or one expensive component taking 80% of the commit.

The browser Performance panel#

Chrome DevTools' Performance panel records everything the browser does: JavaScript, layout, painting and network. React 19.2 adds custom tracks here — a Scheduler track showing what React is working on and at which priority, and a Components track showing component render and effect timings — so you can see React work next to browser work. Use the CPU throttling option (e.g. "4× slowdown") to simulate a cheaper phone.

Measuring in code: <Profiler>#

React's <Profiler> component reports render timings programmatically:

JSX
import { Profiler } from "react";

function onRender(id, phase, actualDuration, baseDuration) {
  if (actualDuration > 16) {
    console.warn(`${id} ${phase} took ${actualDuration.toFixed(1)}ms (no memo: ${baseDuration.toFixed(1)}ms)`);
  }
}

export default function App() {
  return (
    <Profiler id="ProductGrid" onRender={onRender}>
      <ProductGrid />
    </Profiler>
  );
}
  • phase is "mount", "update" or "nested-update".
  • actualDuration is the time spent rendering this commit; baseDuration estimates the cost without any memoisation.

Profiling adds overhead and is disabled in standard production builds, so use it for investigation, not permanently.

Common problems and fixes#

1. Too many re-renders

Symptom: typing in one input re-renders the whole page.

Fixes, in order of preference:

  • Move state down into the component that actually uses it.
  • Pass children so wrappers with state don't re-render their content.
  • Split contexts so updates only reach components that care.
  • Select narrowly from stores (useStore((s) => s.count)).
  • Memoise (memo, useMemo, useCallback) — or enable the React Compiler to do it automatically.

2. Expensive rendering

Symptom: one component takes many milliseconds every render.

JSX
function Results({ query, items }) {
  // ❌ recomputes on every render, even when only unrelated state changed
  const sorted = [...items].sort((a, b) => a.score - b.score).filter((i) => i.name.includes(query));
  return <List items={sorted} />;
}

Fixes: useMemo for the calculation, useDeferredValue so typing stays responsive, or move the work to the server.

3. Huge lists

Rendering thousands of DOM nodes is slow no matter how clever your React code is. Virtualise: only render rows in (or near) the viewport.

Terminal
npm install @tanstack/react-virtual
JSX
import { useRef } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";

export function BigList({ rows }) {
  const parentRef = useRef(null);
  const virtualizer = useVirtualizer({
    count: rows.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 36, // row height in px
  });

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

10,000 rows, but only ~15 DOM elements at a time.

4. Large JavaScript bundles

Symptom: the first load is slow, especially on mobile.

  • Code-split routes and heavy features with lazy (see Suspense & lazy loading).
  • Check what's in your bundle. npm run build prints chunk sizes; a visualiser such as npx vite-bundle-visualizer shows which packages take up space.
  • Prefer smaller dependencies and import only what you use (import debounce from "lodash-es/debounce" rather than the whole library).
Output
dist/assets/index-C1x2y3.js     612.40 kB │ gzip: 189.21 kB
(!) Some chunks are larger than 500 kB after minification.

That warning is your cue to split.

5. Slow images and fonts

  • Set width and height on images to avoid layout shift.
  • Use loading="lazy" for images below the fold, modern formats (WebP/AVIF) and appropriately sized files.
  • Preload critical fonts and use font-display: swap.

6. Request waterfalls

A parent fetches, renders, then its child starts fetching — each request waits for the previous one. Start requests in parallel (Promise.all), fetch in route loaders, or prefetch with TanStack Query.

Core Web Vitals#

Google's Core Web Vitals measure real user experience:

MetricMeasuresGood
LCP (Largest Contentful Paint)Loading: when the main content appears≤ 2.5 s
INP (Interaction to Next Paint)Responsiveness: delay after clicks and key presses≤ 200 ms
CLS (Cumulative Layout Shift)Visual stability: how much content jumps around≤ 0.1

Check them with Lighthouse (in Chrome DevTools) or collect them from real users with the web-vitals package:

JSX
import { onCLS, onINP, onLCP } from "web-vitals";

onCLS(console.log);
onINP(console.log);
onLCP(console.log);

In a real app, send these to your analytics instead of logging them.

A performance checklist#

  • Profiled a production build, with CPU throttling
  • No components re-render on every keystroke without reason
  • Long lists are virtualised or paginated
  • Routes and heavy features are lazy-loaded
  • Images are sized, compressed and lazy-loaded
  • Data requests run in parallel and are cached
  • Lighthouse scores are green for LCP, INP and CLS

Common mistakes#

  • Profiling in development mode and chasing problems that don't exist in production.
  • Adding useMemo everywhere without measuring.
  • Ignoring bundle size because "it's fast on my laptop".
  • Optimising React when the real bottleneck is a slow API or a 5 MB image.
  • Leaving the "Highlight updates" setting on and panicking — a flash means a render happened, not that it was slow.

What's next#

Fast is good; usable by everyone is essential. Next: accessibility in React.

Check your understanding

Quick quiz

0/3 answered
  1. 1.Why should you measure performance with a production build?

  2. 2.What does the React DevTools Profiler show?

  3. 3.A list of 10,000 rows makes scrolling janky. What's the most effective fix?

Finished reading?

Mark this lesson complete to track your progress.