react-doctor-cli-dev 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/backend/.env +3 -0
  2. package/backend/dist/index.js +43 -0
  3. package/backend/dist/middleware/auth.js +16 -0
  4. package/backend/dist/routes/reports.js +93 -0
  5. package/backend/package-lock.json +2000 -0
  6. package/backend/package.json +30 -0
  7. package/backend/src/db.ts +24 -0
  8. package/backend/src/index.ts +49 -0
  9. package/backend/src/middleware/auth.ts +21 -0
  10. package/backend/src/routes/reports.ts +110 -0
  11. package/backend/tsconfig.json +12 -0
  12. package/cli/bin/react-doctor.js +29 -0
  13. package/cli/dist/commands/analyze.js +125 -0
  14. package/cli/dist/commands/full.js +366 -0
  15. package/cli/dist/commands/install.js +138 -0
  16. package/cli/dist/commands/profile.js +166 -0
  17. package/cli/dist/index.js +78 -0
  18. package/cli/dist/ui.js +113 -0
  19. package/cli/package-lock.json +936 -0
  20. package/cli/package.json +34 -0
  21. package/cli/src/commands/analyze.ts +162 -0
  22. package/cli/src/commands/full.ts +574 -0
  23. package/cli/src/commands/install.ts +163 -0
  24. package/cli/src/commands/profile.ts +246 -0
  25. package/cli/src/index.ts +84 -0
  26. package/cli/src/ui.ts +120 -0
  27. package/cli/tsconfig.json +16 -0
  28. package/core/report-compiler/index.ts +359 -0
  29. package/core/report-compiler/test-report-compiler.ts +126 -0
  30. package/core/rule-engine/context-builder.ts +146 -0
  31. package/core/rule-engine/evaluator.ts +131 -0
  32. package/core/rule-engine/index.ts +222 -0
  33. package/core/rule-engine/rules.json +304 -0
  34. package/core/rule-engine/suggestion-builder.ts +209 -0
  35. package/core/rule-engine/test-rule-engine.ts +144 -0
  36. package/core/rule-engine/types.ts +202 -0
  37. package/core/runtime/profiler/browser.ts +121 -0
  38. package/core/runtime/profiler/collectors.ts +216 -0
  39. package/core/runtime/profiler/index.ts +311 -0
  40. package/core/runtime/profiler/porfiler.ts +967 -0
  41. package/core/runtime/profiler/route-scanner.ts +76 -0
  42. package/core/runtime/profiler/score.ts +59 -0
  43. package/core/runtime/profiler/server.ts +115 -0
  44. package/core/runtime/profiler/types.ts +65 -0
  45. package/core/runtime/test-runtime-profiler.ts +226 -0
  46. package/core/static-ana/static/analyzer.ts +145 -0
  47. package/core/static-ana/static/ast-parser.ts +31 -0
  48. package/core/static-ana/static/detectors/console-log.ts +49 -0
  49. package/core/static-ana/static/detectors/dead-code.ts +51 -0
  50. package/core/static-ana/static/detectors/effect-loop.ts +45 -0
  51. package/core/static-ana/static/detectors/index.ts +16 -0
  52. package/core/static-ana/static/detectors/inline-function.ts +59 -0
  53. package/core/static-ana/static/detectors/inline-style.ts +52 -0
  54. package/core/static-ana/static/detectors/large-component.ts +79 -0
  55. package/core/static-ana/static/detectors/missing-key.ts +56 -0
  56. package/core/static-ana/static/detectors/missing-memo.ts +59 -0
  57. package/core/static-ana/static/detectors/prop-drilling.ts +66 -0
  58. package/core/static-ana/static/helpers.ts +81 -0
  59. package/core/static-ana/static/scanner.ts +93 -0
  60. package/core/static-ana/test-analyzer.ts +115 -0
  61. package/core/static-ana/types.ts +25 -0
  62. package/core/tests/mock-react-project/src/app.tsx +22 -0
  63. package/core/tests/mock-react-project/src/components/Button.tsx +9 -0
  64. package/core/tests/mock-react-project/src/components/Header.tsx +3 -0
  65. package/core/tests/mock-react-project/src/components/ListTesting.tsx +51 -0
  66. package/core/tests/mock-react-project/src/components/UserDashboard.tsx +66 -0
  67. package/core/tests/mock-react-project/src/utils.ts +4 -0
  68. package/package.json +55 -0
  69. package/react-doctor-cli-dev-1.0.0.tgz +0 -0
  70. package/shared/dist/index.d.ts +2 -0
  71. package/shared/dist/index.js +19 -0
  72. package/shared/dist/schemas.d.ts +91 -0
  73. package/shared/dist/schemas.js +82 -0
  74. package/shared/dist/types.d.ts +44 -0
  75. package/shared/dist/types.js +2 -0
  76. package/shared/package-lock.json +47 -0
  77. package/shared/package.json +21 -0
  78. package/shared/src/index.ts +4 -0
  79. package/shared/src/schemas.ts +136 -0
  80. package/shared/src/types.ts +137 -0
  81. package/shared/tsconfig.json +15 -0
  82. package/tsconfig.json +25 -0
@@ -0,0 +1,131 @@
1
+ // Evaluates whether a rule's conditions are met given the
2
+ // current EvaluationContext.
3
+ //
4
+ // HOW CONDITION EVALUATION WORKS:
5
+ //
6
+ // A rule condition is a plain JavaScript expression string:
7
+ // "metrics.lcp > 2500"
8
+ // "hasIssue('missing-memo')"
9
+ // "topRerenderCount >= 5 && slowCommitCount > 0"
10
+ //
11
+ // We evaluate these strings by building a Function() that
12
+ // receives the context fields as named parameters:
13
+ //
14
+ // new Function('metrics', 'lcp', ..., 'return metrics.lcp > 2500')
15
+ //
16
+ // This is safe here because:
17
+ // 1. The condition strings come from our own rules.json file,
18
+ // not from user input
19
+ // 2. The context only contains numbers, strings, arrays, and
20
+ // safe helper functions — no file system or network access
21
+ //
22
+ // Each condition is wrapped in a try/catch. If a condition
23
+ // throws (e.g. a typo in rules.json), it logs a warning and
24
+ // returns false so the app doesn't crash.
25
+ // ─────────────────────────────────────────────────────────────
26
+
27
+ import { RuleDefinition, EvaluationContext } from "./types";
28
+
29
+ /**
30
+ * Evaluates a single rule against the current context.
31
+ *
32
+ * Returns true if the rule should fire (i.e. the problem exists).
33
+ * Returns false if the condition is not met or throws an error.
34
+ *
35
+ * For "cross" rules: BOTH the static AND runtime conditions must
36
+ * be true. If either is false, the rule does not fire.
37
+ *
38
+ * For "static" rules: only condition.static is evaluated.
39
+ * For "runtime" rules: only condition.runtime is evaluated.
40
+ */
41
+ export function evaluateRule(
42
+ rule: RuleDefinition,
43
+ context: EvaluationContext,
44
+ ): boolean {
45
+ try {
46
+ // Evaluate the runtime condition if this rule has one
47
+ if (rule.condition.runtime !== undefined) {
48
+ const result = evaluateExpression(rule.condition.runtime, context);
49
+ // For cross rules: if the runtime condition is false, stop here.
50
+ // No need to check the static condition — both must be true.
51
+ if (!result) return false;
52
+ }
53
+
54
+ // Evaluate the static condition if this rule has one
55
+ if (rule.condition.static !== undefined) {
56
+ const result = evaluateExpression(rule.condition.static, context);
57
+ if (!result) return false;
58
+ }
59
+
60
+ // If we reach here, all conditions that exist have passed
61
+ return true;
62
+
63
+ } catch (err) {
64
+ // A condition threw an error — most likely a typo in rules.json.
65
+ // Log it clearly so it can be fixed, but don't crash the engine.
66
+ console.warn(
67
+ `⚠️ Rule Engine: condition evaluation failed for rule "${rule.id}".\n` +
68
+ ` Condition: ${rule.condition.runtime ?? rule.condition.static}\n` +
69
+ ` Error: ${(err as Error).message}\n` +
70
+ ` This rule will be skipped.`,
71
+ );
72
+ return false;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Evaluates a single expression string against the context.
78
+ *
79
+ * HOW IT WORKS:
80
+ * We extract all the keys from the context object, then build a
81
+ * new Function that receives those keys as named parameters and
82
+ * returns the evaluated expression.
83
+ *
84
+ * Example — for the expression "metrics.lcp > 2500":
85
+ * new Function(
86
+ * 'metrics', 'renderTime', 'rerenders', ...,
87
+ * 'return metrics.lcp > 2500'
88
+ * )(context.metrics, context.renderTime, context.rerenders, ...)
89
+ *
90
+ * The context keys become the function's parameter names, and the
91
+ * context values become the arguments. This means any field on
92
+ * the context is directly accessible in the condition string by
93
+ * its name — no "context." prefix needed.
94
+ *
95
+ * @param expression — the condition string from rules.json
96
+ * @param context — the evaluation context built from both reports
97
+ */
98
+ function evaluateExpression(
99
+ expression: string,
100
+ context: EvaluationContext,
101
+ ): boolean {
102
+ // Get all the keys and values from the context
103
+ const keys = Object.keys(context) as (keyof EvaluationContext)[];
104
+ const values = keys.map(k => context[k]);
105
+
106
+ // Build a function: (key1, key2, ...) => expression
107
+ // The 'return' ensures the expression result is returned
108
+ const fn = new Function(...keys, `return (${expression});`);
109
+
110
+ // Call the function with the context values as arguments
111
+ const result = fn(...values);
112
+
113
+ // Coerce to boolean — conditions should always be truthy/falsy
114
+ return Boolean(result);
115
+ }
116
+
117
+ /**
118
+ * Evaluates all rules against the context and returns only
119
+ * the rules that fired (whose conditions were true).
120
+ *
121
+ * This is the main function called by index.ts.
122
+ *
123
+ * @param rules — all rule definitions from rules.json
124
+ * @param context — the assembled evaluation context
125
+ */
126
+ export function evaluateAllRules(
127
+ rules: RuleDefinition[],
128
+ context: EvaluationContext,
129
+ ): RuleDefinition[] {
130
+ return rules.filter(rule => evaluateRule(rule, context));
131
+ }
@@ -0,0 +1,222 @@
1
+ // The RuleEngine class — the public API for the rule engine.
2
+ // This is what the CLI and Report Compiler call.
3
+ //
4
+ // FULL FLOW:
5
+ //
6
+ // 1. RuleEngine.run(staticReport, runtimeReports) is called
7
+ //
8
+ // 2. rules.json is loaded from disk
9
+ // (JSON with comments is stripped before parsing)
10
+ //
11
+ // 3. For each route in the runtime reports:
12
+ // a. buildContext() → assembles the EvaluationContext
13
+ // b. evaluateAllRules() → finds which rules fired
14
+ // c. buildAllSuggestions()→ converts fired rules to Suggestion objects
15
+ //
16
+ // 4. All results are written to core/reports/suggestions.json
17
+ //
18
+ // 5. The RuleEngineResult array is returned for use by the
19
+ // Report Compiler
20
+ // ─────────────────────────────────────────────────────────────
21
+
22
+ import path from "path";
23
+ import fs from "fs-extra";
24
+
25
+ import { StaticReport, RuntimeReport, Suggestion } from "../../shared/src/types";
26
+ import { RuleDefinition, RuleEngineResult } from "./types";
27
+ import { buildContext } from "./context-builder";
28
+ import { evaluateAllRules } from "./evaluator";
29
+ import { buildAllSuggestions } from "./suggestion-builder";
30
+
31
+ export class RuleEngine {
32
+ // Path to the rules.json file — same directory as this file
33
+ private rulesPath: string;
34
+
35
+ // Path to the reports folder where suggestions.json is saved
36
+ private reportDir: string;
37
+
38
+ constructor(outputDir?: string) {
39
+ this.rulesPath = path.join(__dirname, "rules.json");
40
+ this.reportDir = outputDir ?? path.resolve(__dirname, "..", "reports");
41
+ fs.ensureDirSync(this.reportDir);
42
+ }
43
+
44
+ /**
45
+ * Runs the Rule Engine against both reports.
46
+ *
47
+ * @param staticReport — result from the Static Analyzer (can be null
48
+ * if only runtime profiling was run)
49
+ * @param runtimeReports — map of route → RuntimeReport from the profiler
50
+ * Example: { "/": {...}, "/about": {...} }
51
+ * Can be empty {} if only static analysis was run
52
+ *
53
+ * @returns Array of RuleEngineResult — one per route
54
+ */
55
+ async run(
56
+ staticReport: StaticReport | null,
57
+ runtimeReports: Record<string, RuntimeReport>,
58
+ ): Promise<RuleEngineResult[]> {
59
+ console.log("\n🧠 Rule Engine starting...");
60
+
61
+ // ── Load rules from disk ───────────────────────────────────
62
+ const rules = this.loadRules();
63
+ console.log(` Loaded ${rules.length} rules`);
64
+
65
+ const results: RuleEngineResult[] = [];
66
+
67
+ // ── Handle case: only static report, no runtime data ───────
68
+ // If no runtime reports were provided (user ran analyze only),
69
+ // we still run all static-only rules against an empty runtime context.
70
+ if (Object.keys(runtimeReports).length === 0) {
71
+ console.log(" Running static-only rules (no runtime data)...");
72
+
73
+ const context = buildContext(staticReport, null);
74
+ const firedRules = evaluateAllRules(rules, context);
75
+ const suggestions = buildAllSuggestions(firedRules, context);
76
+
77
+ results.push(
78
+ this.buildResult("(static-only)", "none", suggestions),
79
+ );
80
+ } else {
81
+ // ── Run rules for each route ───────────────────────────────
82
+ // The runtime report is keyed by route (and optionally device).
83
+ // Keys look like "/" or "/::desktop" or "/about::mobile".
84
+ for (const [key, runtimeReport] of Object.entries(runtimeReports)) {
85
+ // Parse the key — it may include a device suffix
86
+ const [route, device] = key.includes("::")
87
+ ? key.split("::")
88
+ : [key, runtimeReport.deviceType ?? "desktop"];
89
+
90
+ console.log(`\n Analyzing: ${route} [${device}]`);
91
+
92
+ // Build the evaluation context from both reports
93
+ const context = buildContext(staticReport, runtimeReport);
94
+
95
+ // Find all rules whose conditions are true
96
+ const firedRules = evaluateAllRules(rules, context);
97
+ console.log(` Rules fired: ${firedRules.length} / ${rules.length}`);
98
+
99
+ // Convert fired rules into Suggestion objects
100
+ const suggestions = buildAllSuggestions(firedRules, context);
101
+
102
+ // Log the severity breakdown
103
+ const critical = suggestions.filter(s => s.severity === "critical").length;
104
+ const warning = suggestions.filter(s => s.severity === "warning").length;
105
+ const info = suggestions.filter(s => s.severity === "info").length;
106
+
107
+ if (critical > 0) console.log(` ❌ ${critical} critical`);
108
+ if (warning > 0) console.log(` ⚠️ ${warning} warnings`);
109
+ if (info > 0) console.log(` ℹ️ ${info} info`);
110
+
111
+ results.push(this.buildResult(route, device, suggestions));
112
+ }
113
+ }
114
+
115
+ // ── Save to disk ───────────────────────────────────────────
116
+ await this.saveResults(results);
117
+
118
+ const totalSuggestions = results.reduce((sum, r) => sum + r.suggestions.length, 0);
119
+ console.log(`\n✅ Rule Engine complete. ${totalSuggestions} suggestion(s) generated.`);
120
+
121
+ return results;
122
+ }
123
+
124
+ // ───────────────────────────────────────────────────────────
125
+ // PRIVATE: load rules
126
+ // ───────────────────────────────────────────────────────────
127
+
128
+ /**
129
+ * Loads rules from rules.json and parses them.
130
+ *
131
+ * rules.json uses JavaScript-style comments (// and /* )
132
+ * for readability, but JSON.parse() doesn't support comments.
133
+ * We strip all comment lines before parsing.
134
+ *
135
+ * This lets us write:
136
+ * // RUNTIME-ONLY RULES
137
+ * { "id": "slow-lcp", ... }
138
+ *
139
+ * Instead of raw uncommented JSON which is harder to navigate.
140
+ */
141
+ private loadRules(): RuleDefinition[] {
142
+ if (!fs.existsSync(this.rulesPath)) {
143
+ throw new Error(
144
+ `❌ rules.json not found at: ${this.rulesPath}\n` +
145
+ ` Make sure rules.json is in the same folder as index.ts`,
146
+ );
147
+ }
148
+
149
+ const raw = fs.readFileSync(this.rulesPath, "utf-8");
150
+
151
+ // Strip single-line comments (// ...) and multi-line comments (/* ... */)
152
+ // We do this line by line for single-line comments, then use a regex
153
+ // for multi-line. This is simpler than a full JSON-with-comments parser.
154
+ const stripped = raw
155
+ .split("\n")
156
+ .map(line => {
157
+ // Remove // comments but be careful not to remove // inside strings
158
+ // Simple approach: if the line starts with optional whitespace then //,
159
+ // remove the whole line. This covers the 99% case in rules.json.
160
+ const trimmed = line.trimStart();
161
+ return trimmed.startsWith("//") ? "" : line;
162
+ })
163
+ .join("\n")
164
+ // Remove multi-line block comments /* ... */
165
+ .replace(/\/\*[\s\S]*?\*\//g, "");
166
+
167
+ try {
168
+ return JSON.parse(stripped) as RuleDefinition[];
169
+ } catch (err) {
170
+ throw new Error(
171
+ `❌ Failed to parse rules.json: ${(err as Error).message}\n` +
172
+ ` Check rules.json for syntax errors.`,
173
+ );
174
+ }
175
+ }
176
+
177
+ // ───────────────────────────────────────────────────────────
178
+ // PRIVATE: build result
179
+ // ───────────────────────────────────────────────────────────
180
+
181
+ /**
182
+ * Wraps the suggestions for one route into a RuleEngineResult.
183
+ * Adds summary counts and metadata.
184
+ */
185
+ private buildResult(
186
+ route: string,
187
+ device: string,
188
+ suggestions: Suggestion[],
189
+ ): RuleEngineResult {
190
+ return {
191
+ timestamp: new Date().toISOString(),
192
+ route,
193
+ device,
194
+ suggestions,
195
+ summary: {
196
+ critical: suggestions.filter(s => s.severity === "critical").length,
197
+ warning: suggestions.filter(s => s.severity === "warning").length,
198
+ info: suggestions.filter(s => s.severity === "info").length,
199
+ total: suggestions.length,
200
+ },
201
+ };
202
+ }
203
+
204
+ // ───────────────────────────────────────────────────────────
205
+ // PRIVATE: save results
206
+ // ───────────────────────────────────────────────────────────
207
+
208
+ /**
209
+ * Saves all RuleEngineResults to suggestions.json.
210
+ *
211
+ * The file structure is an array — one entry per route.
212
+ * The Report Compiler reads this file when building the
213
+ * final combined report.
214
+ *
215
+ * Location: core/reports/suggestions.json
216
+ */
217
+ private async saveResults(results: RuleEngineResult[]): Promise<void> {
218
+ const outputPath = path.join(this.reportDir, "suggestions.json");
219
+ await fs.writeJson(outputPath, results, { spaces: 2 });
220
+ console.log(`📄 Suggestions saved to: ${outputPath}`);
221
+ }
222
+ }
@@ -0,0 +1,304 @@
1
+ [
2
+ {
3
+ "id": "slow-lcp",
4
+ "category": "runtime",
5
+ "severity": "critical",
6
+ "title": "LCP is too slow",
7
+ "condition": {
8
+ "runtime": "metrics.lcp > 2500"
9
+ },
10
+ "message": "Your Largest Contentful Paint is {metrics.lcp}ms. Users see a blank screen for too long. The LCP element (usually a hero image or heading) takes over 2.5 seconds to appear.",
11
+ "fix": "1. Add loading='lazy' to images that are NOT above the fold.\n2. Preload your hero image with <link rel='preload' as='image'>.\n3. Reduce the size of your largest image using WebP format.\n4. If the LCP element is text, ensure your font loads quickly."
12
+ },
13
+
14
+ {
15
+ "id": "slow-fcp",
16
+ "category": "runtime",
17
+ "severity": "warning",
18
+ "title": "First Contentful Paint is slow",
19
+ "condition": {
20
+ "runtime": "metrics.fcp > 1800"
21
+ },
22
+ "message": "Your First Contentful Paint is {metrics.fcp}ms. Users see nothing for over 1.8 seconds. The page feels unresponsive before anything appears.",
23
+ "fix": "1. Eliminate render-blocking resources (large CSS or JS in <head>).\n2. Inline critical CSS so the first paint doesn't wait for a network request.\n3. Enable server-side rendering (SSR) if using a static SPA."
24
+ },
25
+
26
+ {
27
+ "id": "slow-ttfb",
28
+ "category": "runtime",
29
+ "severity": "warning",
30
+ "title": "Server response is slow (TTFB)",
31
+ "condition": {
32
+ "runtime": "metrics.ttfb > 800"
33
+ },
34
+ "message": "Your Time to First Byte is {metrics.ttfb}ms. The server takes over 800ms just to start sending data. This delays everything else.",
35
+ "fix": "1. Check your backend API response times.\n2. Add server-side caching (Redis, CDN).\n3. Move your server closer to your users with edge deployment."
36
+ },
37
+
38
+ {
39
+ "id": "layout-shift",
40
+ "category": "runtime",
41
+ "severity": "warning",
42
+ "title": "Page has layout shift (CLS)",
43
+ "condition": {
44
+ "runtime": "metrics.cls > 0.1"
45
+ },
46
+ "message": "Your Cumulative Layout Shift score is {metrics.cls}. Elements on the page are jumping around while it loads, which causes accidental clicks and a frustrating experience.",
47
+ "fix": "1. Add explicit width and height attributes to all images and videos.\n2. Reserve space for ads and dynamically injected content.\n3. Avoid inserting content above existing content after load."
48
+ },
49
+
50
+ {
51
+ "id": "slow-inp",
52
+ "category": "runtime",
53
+ "severity": "warning",
54
+ "title": "Page is slow to respond to clicks (INP)",
55
+ "condition": {
56
+ "runtime": "metrics.inp > 200"
57
+ },
58
+ "message": "Your Interaction to Next Paint is {metrics.inp}ms. When users click, the page takes too long to visually respond.",
59
+ "fix": "1. Break up long JavaScript tasks (anything over 50ms).\n2. Use Web Workers for heavy computation.\n3. Debounce expensive event handlers.\n4. Reduce the amount of work React does on each state update."
60
+ },
61
+
62
+ {
63
+ "id": "slow-render-time",
64
+ "category": "runtime",
65
+ "severity": "critical",
66
+ "title": "Page takes too long to become interactive",
67
+ "condition": {
68
+ "runtime": "renderTime > 4000"
69
+ },
70
+ "message": "Your page takes {renderTime}ms to fully load. Users on slower connections will abandon the page before it finishes.",
71
+ "fix": "1. Use React.lazy() and Suspense for route-level code splitting.\n2. Remove unused npm packages to reduce bundle size.\n3. Enable gzip or Brotli compression on your server.\n4. Move large data fetches out of the initial render path."
72
+ },
73
+
74
+ {
75
+ "id": "slow-react-commit",
76
+ "category": "runtime",
77
+ "severity": "warning",
78
+ "title": "React commits are exceeding 16ms budget",
79
+ "condition": {
80
+ "runtime": "slowCommitCount > 0"
81
+ },
82
+ "message": "React had {slowCommitCount} commit(s) that took over 16ms (the 60fps budget). The slowest was {commitDuration}ms. This causes visible frame drops and janky animations.",
83
+ "fix": "1. Use React.memo() on components that receive the same props often.\n2. Use useMemo() for expensive calculations in render.\n3. Use useCallback() to stabilize function references passed as props.\n4. Avoid creating new objects or arrays directly in JSX."
84
+ },
85
+
86
+ {
87
+ "id": "excessive-rerenders",
88
+ "category": "runtime",
89
+ "severity": "warning",
90
+ "title": "Components are re-rendering too many times",
91
+ "condition": {
92
+ "runtime": "topRerenderCount >= 5"
93
+ },
94
+ "message": "The component '{component}' re-rendered {rerenders} times during page load. This is unnecessary work that slows down the UI.",
95
+ "fix": "1. Wrap '{component}' in React.memo() to skip re-renders when props haven't changed.\n2. Check if the parent component is creating new object/array references on every render.\n3. Use the React DevTools Profiler tab to see what triggered each re-render."
96
+ },
97
+
98
+ {
99
+ "id": "high-dom-nodes",
100
+ "category": "runtime",
101
+ "severity": "warning",
102
+ "title": "Too many DOM nodes",
103
+ "condition": {
104
+ "runtime": "domNodes > 1500"
105
+ },
106
+ "message": "Your page has {domNodes} DOM elements. Large DOMs slow down style calculations, layout, and garbage collection.",
107
+ "fix": "1. Use virtualization (react-window or react-virtual) for long lists.\n2. Remove hidden elements from the DOM instead of using display:none.\n3. Split complex pages into multiple routes."
108
+ },
109
+
110
+ {
111
+ "id": "heavy-payload",
112
+ "category": "runtime",
113
+ "severity": "warning",
114
+ "title": "Page payload is too heavy",
115
+ "condition": {
116
+ "runtime": "payloadMB > 3"
117
+ },
118
+ "message": "Your page downloads {payloadMB}MB of assets. On a 3G connection this would take over 15 seconds.",
119
+ "fix": "1. Enable code splitting with React.lazy().\n2. Compress images and use WebP/AVIF formats.\n3. Remove unused npm dependencies.\n4. Audit your bundle with a tool like webpack-bundle-analyzer."
120
+ },
121
+
122
+ {
123
+ "id": "js-errors-detected",
124
+ "category": "runtime",
125
+ "severity": "critical",
126
+ "title": "JavaScript errors during page load",
127
+ "condition": {
128
+ "runtime": "errorCount > 0"
129
+ },
130
+ "message": "React Doctor detected {errorCount} JavaScript error(s) while loading the page. These errors can break functionality for real users.",
131
+ "fix": "Open your browser DevTools console and fix all red error messages before optimizing performance. Errors are more important than performance metrics."
132
+ },
133
+
134
+ {
135
+ "id": "console-logs-in-production",
136
+ "category": "static",
137
+ "severity": "warning",
138
+ "title": "console.log statements left in code",
139
+ "condition": {
140
+ "static": "hasIssue('console-log')"
141
+ },
142
+ "message": "console.log statements were found in your components. These clutter the browser console in production and can expose sensitive data.",
143
+ "fix": "Remove all console.log() calls or replace them with a proper logging library that can be disabled in production."
144
+ },
145
+
146
+ {
147
+ "id": "missing-list-keys",
148
+ "category": "static",
149
+ "severity": "critical",
150
+ "title": "Missing key props in lists",
151
+ "condition": {
152
+ "static": "hasIssue('missing-key')"
153
+ },
154
+ "message": "Components are rendering lists without key props. React uses keys to track which items changed — without them, React re-renders the entire list on every update.",
155
+ "fix": "Add a unique key prop to every element inside a .map() call. Use the item's ID, not its index: key={item.id} not key={index}."
156
+ },
157
+
158
+ {
159
+ "id": "inline-styles-detected",
160
+ "category": "static",
161
+ "severity": "info",
162
+ "title": "Inline styles in components",
163
+ "condition": {
164
+ "static": "hasIssue('inline-style')"
165
+ },
166
+ "message": "Components are using inline style objects. Every render creates a new object reference, which prevents React from skipping re-renders even when nothing visually changed.",
167
+ "fix": "Move styles to a CSS file, CSS module, or define them outside the component so the reference stays stable between renders."
168
+ },
169
+
170
+ {
171
+ "id": "prop-drilling-detected",
172
+ "category": "static",
173
+ "severity": "warning",
174
+ "title": "Deep prop drilling detected",
175
+ "condition": {
176
+ "static": "hasIssue('prop-drilling')"
177
+ },
178
+ "message": "Props are being passed through multiple component layers to reach their destination. This makes components tightly coupled and hard to refactor.",
179
+ "fix": "Use React Context for data that many components need (theme, user, settings). For complex state, consider Zustand or Redux Toolkit."
180
+ },
181
+
182
+ {
183
+ "id": "dead-code-detected",
184
+ "category": "static",
185
+ "severity": "info",
186
+ "title": "Dead code found",
187
+ "condition": {
188
+ "static": "hasIssue('dead-code')"
189
+ },
190
+ "message": "Unused variables or imports were found in your components. These add to your bundle size for no benefit.",
191
+ "fix": "Remove unused imports and variables. Enable ESLint's no-unused-vars rule to catch these automatically."
192
+ },
193
+
194
+ {
195
+ "id": "effect-loop-risk",
196
+ "category": "static",
197
+ "severity": "critical",
198
+ "title": "Potential infinite loop in useEffect",
199
+ "condition": {
200
+ "static": "hasIssue('effect-loop')"
201
+ },
202
+ "message": "A useEffect hook has a dependency that it also modifies, which can cause an infinite re-render loop. This crashes the browser tab.",
203
+ "fix": "Check your useEffect dependency arrays. If the effect sets state, make sure that state is not also in the dependency array — or use a ref instead."
204
+ },
205
+
206
+ {
207
+ "id": "missing-memo-with-rerenders",
208
+ "category": "cross",
209
+ "severity": "critical",
210
+ "title": "Unmemoized component is re-rendering excessively",
211
+ "condition": {
212
+ "static": "hasIssue('missing-memo')",
213
+ "runtime": "topRerenderCount >= 5"
214
+ },
215
+ "message": "The static analyzer found a component without React.memo(), AND the profiler measured that '{component}' re-rendered {rerenders} times. Without memoization, every parent render forces this component to re-render unnecessarily.",
216
+ "fix": "Wrap '{component}' in React.memo():\n\nexport default React.memo(function {component}(props) {\n // your existing component code\n});\n\nThis tells React to skip re-rendering if props haven't changed."
217
+ },
218
+
219
+ {
220
+ "id": "large-component-with-slow-commit",
221
+ "category": "cross",
222
+ "severity": "critical",
223
+ "title": "Large component is causing slow React commits",
224
+ "condition": {
225
+ "static": "hasIssue('large-component')",
226
+ "runtime": "slowestCommit > 50"
227
+ },
228
+ "message": "The static analyzer found a large component (over 300 lines), AND the profiler measured a React commit taking {commitDuration}ms — over 3x the 16ms budget. Large components force React to re-evaluate more code per commit.",
229
+ "fix": "Split your largest component into smaller ones and use React.lazy() to load them on demand:\n\nconst HeavySection = React.lazy(() => import('./HeavySection'));\n\n<Suspense fallback={<Loading />}>\n <HeavySection />\n</Suspense>"
230
+ },
231
+
232
+ {
233
+ "id": "inline-functions-with-high-rerenders",
234
+ "category": "cross",
235
+ "severity": "warning",
236
+ "title": "Inline functions are causing unnecessary re-renders",
237
+ "condition": {
238
+ "static": "hasIssue('inline-function')",
239
+ "runtime": "topRerenderCount >= 3"
240
+ },
241
+ "message": "Inline functions were found in JSX, AND '{component}' re-rendered {rerenders} times. Inline functions create a new reference on every render, which breaks React.memo() and triggers child re-renders.",
242
+ "fix": "Move event handlers outside of JSX and wrap them in useCallback:\n\nconst handleClick = useCallback(() => {\n // your handler code\n}, [dependency]);\n\n// Then use: <Button onClick={handleClick} />"
243
+ },
244
+
245
+ {
246
+ "id": "missing-memo-with-slow-commit",
247
+ "category": "cross",
248
+ "severity": "warning",
249
+ "title": "Missing memoization is slowing React commits",
250
+ "condition": {
251
+ "static": "hasIssue('missing-memo')",
252
+ "runtime": "avgCommit > 16"
253
+ },
254
+ "message": "Components are missing React.memo(), AND React commits are averaging {avgCommit}ms — above the 16ms 60fps budget. Without memoization, React re-renders everything on every update.",
255
+ "fix": "Add React.memo() to pure functional components that receive stable props. Also review useMemo() for expensive computations inside render."
256
+ },
257
+
258
+ {
259
+ "id": "prop-drilling-with-rerenders",
260
+ "category": "cross",
261
+ "severity": "warning",
262
+ "title": "Prop drilling is causing cascade re-renders",
263
+ "condition": {
264
+ "static": "hasIssue('prop-drilling')",
265
+ "runtime": "topRerenderCount >= 5"
266
+ },
267
+ "message": "Deep prop drilling was detected in the code, AND components are re-rendering {rerenders} times. When a top-level prop changes, every component in the drilling chain re-renders.",
268
+ "fix": "Replace prop drilling with React Context or a state management library (Zustand). This breaks the re-render cascade by letting only subscribed components update."
269
+ },
270
+
271
+ {
272
+ "id": "js-errors-with-slow-lcp",
273
+ "category": "runtime",
274
+ "severity": "critical",
275
+ "title": "JavaScript errors are blocking page rendering",
276
+ "condition": {
277
+ "runtime": "errorCount > 0 && metrics.lcp > 2500"
278
+ },
279
+ "message": "JavaScript errors were detected during load AND your LCP is {metrics.lcp}ms. JS errors can interrupt the render cycle, delaying when the largest element appears.",
280
+ "fix": "Fix all JavaScript errors first — they are blocking your render path. After fixing errors, re-run React Doctor to get accurate performance measurements."
281
+ },
282
+ {
283
+ "id": "inline-functions-detected",
284
+ "category": "static",
285
+ "severity": "info",
286
+ "title": "Inline functions found in JSX props",
287
+ "condition": {
288
+ "static": "hasIssue('inline-function')"
289
+ },
290
+ "message": "Inline arrow functions were found directly inside JSX props. Every render creates a brand new function reference, which prevents React.memo() from working correctly on child components and can trigger unnecessary re-renders.",
291
+ "fix": "Move event handlers outside the JSX and wrap them with useCallback:\n\nconst handleClick = useCallback(() => {\n // your handler\n}, [dependency]);\n\n// Then use:\n<Button onClick={handleClick} />"
292
+ },
293
+ {
294
+ "id": "low-performance-score",
295
+ "category": "runtime",
296
+ "severity": "warning",
297
+ "title": "Overall performance score is low",
298
+ "condition": {
299
+ "runtime": "performanceScore < 50"
300
+ },
301
+ "message": "Your overall performance score is {performanceScore}/100. Multiple metrics are underperforming simultaneously. A score below 50 means users are experiencing a noticeably slow and frustrating experience.",
302
+ "fix": "Review all other suggestions in this report and fix critical issues first. Start with JS errors, then LCP, then React commit durations. Re-run React Doctor after each fix to track your progress."
303
+ }
304
+ ]