DEV Community

Cover image for Static Analysis Told Me My React App Had a Problem. Runtime Profiling Told Me Which One
OzmaKa
OzmaKa

Posted on

Static Analysis Told Me My React App Had a Problem. Runtime Profiling Told Me Which One

You open Lighthouse. Score isn't great!

You open React DevTools. Components are re-rendering, a lot.

You open the code. Inline functions, effects, prop chains, oversized components — any of them could be the problem.

So you guess. React.memo() here. useCallback() there. Move something into useMemo(). Run it again.

Did that fix anything? You genuinely don't know. You just made a change and hoped.

That loop is what pushed me to build React Doctor.

The actual gap

Lighthouse, React DevTools, and static analyzers are all good at their jobs. The problem is they don't talk to each other.

A static analyzer sees this and flags it:

function ProductList({ products }) {
  return (
    <div>
      {products.map(product => (
        <ProductCard
          key={product.id}
          product={product}
          onSelect={() => selectProduct(product.id)}
        />
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

"Inline function in render." Fine — but that's true of thousands of components that never cause a problem. The linter has no idea if this component renders twice a session or two hundred times while someone types in a search box. It's a pattern match, not a diagnosis.

What React Doctor does differently

Two passes, correlated:

Static pass — Babel AST analysis over your JSX/TSX. Inline functions, missing memo opportunities, prop drilling, effect issues, missing keys, dead code.

Runtime pass — launches your app in a real browser via Puppeteer and profiles it. LCP, FCP, CLS, INP, TTFB, plus React commit durations and actual render counts per component.

A rule engine then cross-references the two. A flagged pattern that never fires at runtime gets deprioritized. A flagged pattern sitting in a component that re-rendered dozens of times during a short session gets surfaced first.

Here's what that looks like on a real flagged component — condensed output from a repo with a search input wired directly to a filtered list:

⚠ HIGH PRIORITY — src/components/ProductList.jsx:8

  Static finding:
    Inline arrow function passed as onSelect prop (line 12)

  Runtime evidence:
    Component subtree re-rendered 47 times during 12s session
    Avg commit duration: 4.2ms | Total: 197ms
    Re-renders correlate with parent state updates (searchQuery)

  Diagnosis:
    The inline callback creates a new function reference on every
    parent render. ProductCard is not memoized, so it re-renders
    on every keystroke in the search input regardless of whether
    its own props changed.

  Suggested fix:
    Wrap the callback in useCallback, and wrap ProductCard in
    React.memo. Given the render count, this should meaningfully
    reduce work during typing.
Enter fullscreen mode Exit fullscreen mode

That's the difference. Not "this pattern looks bad," but "this pattern looks bad and here's the runtime proof it's costing you something."

The same logic holds for React.memo() calls, oversized-component warnings, and most other static suggestions — a 300-line component that renders once on mount costs nothing; a 40-line component re-rendering 60 times a second is the actual fire. React Doctor prioritizes based on what a component actually did while your app was running, not its line count.

It also answers the question Lighthouse can't

Lighthouse gives you INP: 280ms and stops there. It can't tell you if that's from an oversized component, a runaway effect, excessive re-renders, or bloated DOM. You're still guessing which part of your React code to open first. React Doctor's job is that next step — pointing at the actual component and the actual reason.

Try it

npm install -g react-doctor-cli-dev
react-doctor full ./my-app --upload
Enter fullscreen mode Exit fullscreen mode

It's early and open source — issues, stars, and "this broke on my codebase" reports all genuinely help, don't hesitate to give a feedback 💻☕

Curious how others handle this: when Lighthouse or DevTools flags something, how do you decide if it's actually worth fixing versus noise? Combination of tools, gut feeling, something else?

Top comments (0)