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>
);
}
"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.
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
- GitHub: https://github.com/softar-dev/React_Doctor
- npm: https://www.npmjs.com/package/react-doctor-cli-dev
- Docs: https://react-doctor-cli.web.app
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)