Skip to main content

Command Palette

Search for a command to run...

Using the React Profiler โ€” Finding Real Bottlenecks in Real Apps

Day 25 of 40 โ€” React System Design Series

Updated
โ€ข8 min read
Using the React Profiler โ€” Finding Real Bottlenecks in Real Apps
R

Hi, Iโ€™m Richa โ€” a Senior Frontend Engineer with 5+ years of experience building scalable, production-grade web interfaces for enterprise and consumer applications. I work primarily with React, TypeScript, and modern frontend architectures, focusing on component systems, performance, and maintainability. Most of my experience comes from building real-world products in regulated domains like banking and insurance, where clarity, reliability, and long-term ownership matter more than quick demos. Through this blog, I write about frontend engineering fundamentals, scalable UI design, problem-solving, and the lessons Iโ€™ve learned working on large codebases. My goal is to share practical insights โ€” not shortcuts โ€” for developers who want to grow strong engineering foundations. I also mentor early-career developers and strongly believe that curiosity, asking the right questions, and understanding why something works are more important than memorizing tools. If youโ€™re serious about improving as an engineer, youโ€™re in the right place.


Hey everyone, Richa here! ๐Ÿ‘‹

profiler gif

Day 25. Last day of Week 5.

And today we wrap up the performance week with the tool that ties everything together.

The React Profiler.

Here's the thing โ€” everything we've covered this week (code splitting, virtualisation, bundle optimisation, memoisation on Day 5) has one thing in common: you should only apply them where there's an actual, measurable problem.

The React Profiler is how you find those problems. It shows you exactly which components are rendering, why they're rendering, and how long each render takes.

Without it, you're guessing. With it, you're diagnosing.

profiler gif

Two profilers โ€” know both

profiler gif

React DevTools Profiler โ€” the browser extension, for runtime render profiling

The <Profiler> API โ€” built into React, for measuring programmatically

We'll cover both. Start with the DevTools one โ€” it's what you'll use 90% of the time.


React DevTools Profiler โ€” getting started

profiler gif

Install the React DevTools browser extension (Chrome or Firefox). It adds two tabs to DevTools: Components and Profiler.

The Profiler tab workflow:

  1. Open DevTools โ†’ Profiler tab

  2. Click the Record button (circle)

  3. Interact with your app โ€” navigate, click, type

  4. Click Stop (square)

  5. Inspect the flame graph

The flame graph shows every render that happened during the recording โ€” which component rendered, how long it took, and what triggered it.


Reading the flame graph

App (0.1ms)
  โ””โ”€โ”€ Layout (0.2ms)
        โ”œโ”€โ”€ Navbar (0.1ms)
        โ”œโ”€โ”€ Sidebar (0.3ms)
        โ””โ”€โ”€ ProductsPage (12.4ms) โ† this is slow
              โ”œโ”€โ”€ ProductFilters (0.4ms)
              โ””โ”€โ”€ ProductGrid (11.8ms) โ† almost all the time is here
                    โ”œโ”€โ”€ ProductCard (0.2ms)
                    โ”œโ”€โ”€ ProductCard (0.2ms)
                    โ”œโ”€โ”€ ProductCard (0.2ms)
                    ... ร— 200 cards

What the colours mean:

  • Grey โ€” component didn't re-render this cycle

  • Blue/teal โ€” rendered, reasonable time

  • Yellow/orange โ€” rendered, slower

  • Red โ€” rendered, slow โ€” investigate

What to look for:

  • Components that are grey when they should be (not re-rendering unnecessarily) โœ…

  • Components that are coloured every time even when nothing changed โŒ

  • Components with unexpectedly long render times

  • Components that render many times for one user action


Finding unnecessary re-renders

profiler gif

This is the most common use case. You suspect a component is re-rendering too often. The Profiler tells you exactly when and why.

After recording, click on any rendered component in the flame graph. The right panel shows:

Why did this render?
  โ— Props changed
      โ—‹ onAddToCart: [function] โ†’ [function]  โ† different function reference!

This tells you: ProductCard re-rendered because onAddToCart was a new function reference every time the parent rendered.

The fix from Day 5: useCallback on the function.

// โŒ Before โ€” new function reference every render
function ProductGrid({ products }: { products: Product[] }) {
  const addToCart = useCartStore(state => state.addItem);

  return products.map(p => (
    <ProductCard
      key={p.id}
      product={p}
      onAddToCart={() => addToCart(p)} // โ† new arrow function every render
    />
  ));
}

// โœ… After โ€” stable function reference
function ProductGrid({ products }: { products: Product[] }) {
  const addToCart = useCartStore(state => state.addItem);

  const handleAddToCart = useCallback((product: Product) => {
    addToCart(product);
  }, [addToCart]);

  return products.map(p => (
    <MemoizedProductCard
      key={p.id}
      product={p}
      onAddToCart={handleAddToCart}
    />
  ));
}

const MemoizedProductCard = React.memo(ProductCard);

Profile again โ€” the ProductCard components should now be grey (not re-rendering) when the parent renders for unrelated reasons.


The <Profiler> API โ€” measuring in code

For more targeted measurement โ€” especially when you want numbers logged to the console or sent to monitoring:

import { Profiler, type ProfilerOnRenderCallback } from 'react';

const onRenderCallback: ProfilerOnRenderCallback = (
  id,           // the "id" prop of the Profiler
  phase,        // "mount" or "update"
  actualDuration,   // time spent rendering the committed update
  baseDuration,     // estimated time to render entire subtree without memoisation
  startTime,    // when React began rendering the update
  commitTime,   // when React committed the update
) => {
  // Log slow renders
  if (actualDuration > 16) { // 16ms = 1 frame at 60fps
    console.warn(`Slow render: \({id} took \){actualDuration.toFixed(2)}ms (${phase})`);
  }

  // Or send to monitoring (Datadog, Sentry, custom analytics)
  if (actualDuration > 50) {
    analytics.track('slow_render', {
      component: id,
      phase,
      duration: actualDuration,
    });
  }
};

function ProductsPage() {
  return (
    <Profiler id="ProductsPage" onRender={onRenderCallback}>
      <ProductFilters />
      <ProductGrid />
    </Profiler>
  );
}

actualDuration > 16ms means the component is slower than one frame at 60fps โ€” a good threshold for "investigate this."


A complete profiling workflow โ€” finding and fixing a real issue

Here's how I approach a performance complaint in production:

Step 1 โ€” Reproduce the issue

User says: "the dashboard feels slow when I type in the search box."

Open DevTools โ†’ Profiler. Start recording. Type in the search box. Stop.

Step 2 โ€” Read the flame graph

DashboardPage (0.3ms)
  SearchBar (0.1ms)           โ† the component being typed in
  StatsSummary (4.2ms)        โ† why is this re-rendering?
  UserActivityChart (8.7ms)   โ† and this?
  RecentOrdersTable (11.3ms)  โ† and this!

Three heavy components re-rendering on every keystroke in the search bar.

Step 3 โ€” Check why they're re-rendering

Click StatsSummary in the Profiler. "Why did this render?"

Why did this render?
  โ— Parent component rendered

DashboardPage re-renders when the search state changes โ†’ all its children re-render.

Step 4 โ€” Apply the fix

// Before โ€” everything re-renders when searchQuery changes
function DashboardPage() {
  const [searchQuery, setSearchQuery] = useState('');

  return (
    <div>
      <SearchBar value={searchQuery} onChange={setSearchQuery} />
      <StatsSummary />        {/* re-renders on every keystroke */}
      <UserActivityChart />   {/* re-renders on every keystroke */}
      <RecentOrdersTable />   {/* re-renders on every keystroke */}
    </div>
  );
}

// After โ€” heavy components memoised, only re-render if their props change
const MemoStatsSummary = React.memo(StatsSummary);
const MemoUserActivityChart = React.memo(UserActivityChart);
const MemoRecentOrdersTable = React.memo(RecentOrdersTable);

function DashboardPage() {
  const [searchQuery, setSearchQuery] = useState('');

  return (
    <div>
      <SearchBar value={searchQuery} onChange={setSearchQuery} />
      <MemoStatsSummary />        {/* only re-renders if its own props change */}
      <MemoUserActivityChart />
      <MemoRecentOrdersTable />
    </div>
  );
}

Step 5 โ€” Profile again to verify

Record. Type in the search box. Stop.

DashboardPage (0.3ms)
  SearchBar (0.1ms)           โ† still updating (correct)
  StatsSummary (grey)         โ† not re-rendering โœ…
  UserActivityChart (grey)    โ† not re-rendering โœ…
  RecentOrdersTable (grey)    โ† not re-rendering โœ…

Fixed. And we can see it in the Profiler โ€” not just assume it.


Performance marks โ€” custom timing

For more precise measurement of specific operations:

// Mark the start and end of a user interaction
function handleSearch(query: string) {
  performance.mark('search-start');

  setSearchQuery(query);

  // After React renders
  requestAnimationFrame(() => {
    performance.mark('search-end');
    performance.measure('search-render', 'search-start', 'search-end');

    const measure = performance.getEntriesByName('search-render')[0];
    console.log(`Search render: ${measure.duration.toFixed(2)}ms`);
  });
}

You can see these custom marks in DevTools โ†’ Performance tab alongside React's own renders.


The golden rules of profiling

profiler gif

Rule 1 โ€” Always measure before optimising

Don't add React.memo or useMemo because you think it might help. Profile first. Confirm the problem. Then apply the fix. Then profile again to confirm the fix worked.

Rule 2 โ€” Profile on realistic hardware

Chrome DevTools โ†’ Performance tab has a CPU throttle setting. Set it to 4x or 6x slowdown to simulate mid-range Android performance. A fast MacBook Pro hides performance problems that real users face.

Rule 3 โ€” Profile in production mode

React's development mode has extra overhead (warnings, checks, etc.). Profile your production build for accurate numbers:

npm run build
npm run preview  # serves the production build locally

Rule 4 โ€” Fix the worst first

The Profiler shows everything. Start with the slowest component or most frequent unnecessary render. Fix that. Profile again. Repeat.

Rule 5 โ€” Grey is good

In the flame graph โ€” grey means "didn't render." More grey = fewer unnecessary renders. After a round of optimisation, you want to see much more grey than before.


Week 5 wrap-up

profiler gif

This week we covered:

  • Day 21 โ€” Rendering patterns: CSR, SSR, SSG, ISR โ€” the decision framework

  • Day 22 โ€” Code splitting: React.lazy, Suspense, dynamic imports

  • Day 23 โ€” List virtualisation: TanStack Virtual for 5,000+ row lists

  • Day 24 โ€” Bundle optimisation: tree shaking, lighter libraries, the visualiser

  • Day 25 โ€” React Profiler: finding and fixing real render bottlenecks

Next week: System Design Interview Patterns โ€” the scenarios they actually ask at senior/staff interviews. Real problems, real solutions.

week done gif

See you Monday for Day 26 โ€” System design: Design a social media feed (infinite scroll, caching).


โ† Day 24 โ€” Bundle Optimisation โ†’ Day 26 โ€” Design a Social Media Feed


Part of the React System Design Series โ€” 40 days, one topic per day.

React System Design โ€” Learning in Public

Part 25 of 34

I'm a React developer with 5.5 years of experience who spent 2 years in Lit.js โ€” and slowly forgot how React really works at a senior level. This series is my public journey back. Every day I cover one topic: state management, authentication, performance, system design interviews, testing โ€” the real stuff that separates a mid-level dev from a senior. Written honestly, with real code, for developers at every level.

Up next

System Design: Design a Social Media Feed

Day 26 of 40 โ€” React System Design Series