A story about building something real, shipping it, and watching strangers use it.
A few months ago, my team and I were looking for a graduation project idea. We wanted to build something that actually solved a real problem — not a to-do app, not a clone of something that already existed.
We landed on a question that every React developer eventually asks:
"Why is my app slow, and where exactly is the problem?"
The answer is usually scattered across three or four different tools. ESLint catches some code smells. Lighthouse measures some runtime metrics. React DevTools shows component re-renders. None of them talk to each other, and none of them give you a single, prioritized list of things to fix.
So we built one tool that does all of it at once. We called it React Doctor.
What React Doctor Does
React Doctor is a CLI tool written in TypeScript that runs in Node.js. You point it at any React project and run one command:
react-doctor full ./my-app
It then executes four steps automatically:
Step 1 — Static Analysis
It reads every .jsx and .tsx file in your project and parses them into an Abstract Syntax Tree using Babel. Nine specialized detectors walk the tree looking for real problems:
-
useEffecthooks with no dependency array (infinite loop risk) - Components missing
React.memo()that would benefit from it - Prop drilling through multiple component layers
- Inline functions recreated on every render
- Dead code and unused imports
- Oversized components (300+ lines)
- Missing
keyprops in lists -
console.logcalls left in production code - Large style objects defined directly in JSX
Every issue comes back with the exact file, line number, and severity.
Step 2 — Runtime Profiling
This is where it goes further than a linter ever could. React Doctor spins up your dev server, launches a headless Chrome instance through Puppeteer, and injects a hook directly into React's DevTools global hook — before the app even renders.
From there it captures:
- All 5 Core Web Vitals (LCP, FCP, CLS, INP, TTFB)
- React's actual commit durations
- Which components re-rendered and how many times
- DOM node count, memory usage, heaviest asset
- JavaScript errors captured during load
It can also simulate mobile viewports, CPU throttling, and slow network conditions:
react-doctor full ./my-app --mobile --cpu 4 --throttle slow4g
Step 3 — Rule Engine
This is the part I'm proudest of architecturally. 25 rules split into three categories:
- Static rules — fire on code patterns alone
- Runtime rules — fire on performance metrics alone
- Cross rules — require BOTH conditions to be true simultaneously
A cross rule might only fire when a component is missing React.memo() AND was measured re-rendering more than 5 times in the actual browser session. Neither the static analyzer nor the profiler alone would catch that pattern. Combining them does.
Step 4 — Report Compiler
Everything gets merged into four JSON files saved in .react-doctor/ inside your project:
.react-doctor/
├── staticreport.json
├── runtimereport.json
├── suggestions.json
└── finalreport.json
The Dashboard
We also built a backend (Express + SQLite) and a dashboard. Running:
react-doctor full ./my-app --upload
Automatically starts the backend if it isn't running, uploads the report, and opens the dashboard in your browser — straight to that specific report.
The dashboard shows Web Vitals as color-coded cards, issues filterable by severity, screenshots taken at key load moments, and a score history chart so you can watch your fixes actually move the needle across multiple runs.
The Real Challenges
The bugs we hit were more educational than the features we built:
Cross-platform path hell. Windows and Linux handle Chrome paths, shell processes, and file paths completely differently. What worked on one broke silently on the other.
TypeScript compilation target. ts-node defaults to ES3. That caused a __spreadArray is not defined error at runtime that took real debugging to trace. Fixed by explicitly setting target: "ES2020".
The output path bug. On Linux, the final report was writing to the global npm installation directory instead of the user's project folder. Fixed by passing outputDir explicitly through the entire pipeline.
API URL mismatch. Our CLI was posting to /api/report/upload (singular) but the backend served /api/reports/upload (plural). This caused a 401 error that looked like an auth problem but was actually a routing problem.
SQLite and screenshots. Storing screenshots as base64 strings inside SQLite made the database enormous. We switched to saving them as separate PNG files and storing only the path — obvious in retrospect, not obvious at 2am.
The Number That Surprised Us
We published to npm as react-doctor-cli-dev. We shared it with our classmates, posted it on LinkedIn, and moved on to other things.
Then the downloads started coming in from people we didn't know.
2,730+ downloads. 25+ countries.
United States (~764), Germany (~218), United Kingdom (~191), China (~164), France (~137) — and 20 more countries.
We didn't run ads. We didn't post on Reddit. We didn't do a Product Hunt launch. The downloads came from developers finding the package organically on npm and deciding it was worth trying.
That number means more to me than any grade.
Try It
npm install -g react-doctor-cli-dev
react-doctor full ./your-react-app
Requires Node.js 18+ and Chrome. Works on Windows, Linux, and macOS.
A Small Ask
React Doctor is completely free and open source. If it saves you time, consider:
- ⭐ Starring the repo on GitHub — it helps more developers find it
- ☕ Supporting the project — any amount helps cover domain costs and keeps development going
Built by Oussamah Kabalan, Shaheen Sharba — software engineering students at Homs University, Syria.
Top comments (0)