react-performance-advisor 1.0.6 → 1.0.8
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.
- package/README.md +113 -0
- package/package.json +1 -1
- package/dist/index.d.mts +0 -12
- package/dist/index.d.ts +0 -12
- package/dist/index.js +0 -329
- package/dist/index.mjs +0 -307
- package/readme.md +0 -79
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# ⚡ React Performance Advisor
|
|
2
|
+
|
|
3
|
+
> An enterprise-grade React performance monitoring tool powered by AI.
|
|
4
|
+
|
|
5
|
+
**React Performance Advisor** acts as an "X-ray machine" for your React applications. It hooks directly into React's native rendering engine to catch unnecessary re-renders, prop instability, and main-thread blocking — then uses AI to instantly write the code to fix them.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ✨ Features
|
|
10
|
+
|
|
11
|
+
- 🧠 **AI-Powered Diagnosis** — Automatically streams context (render counts, lag time, changed props) to AI to generate exact `useMemo`, `useCallback`, or `React.memo` code fixes.
|
|
12
|
+
- ⏱️ **Millisecond Lag Tracking** — Utilizes React's native `<Profiler>` to catch components taking >16ms to render, identifying main-thread blocking.
|
|
13
|
+
- 🔍 **Deep Prop Tracking** — Performs strict equality checks to catch "Object/Function Reference Traps" causing silent cascading re-renders.
|
|
14
|
+
- 📦 **Smart Aggregation** — Groups identical bugs across massive lists/grids into a single, clean UI warning to prevent log spam.
|
|
15
|
+
- 🔒 **Secure & Local** — Your API key is stored securely in your browser's Local Storage. It never touches your source code.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 📦 Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install react-performance-advisor
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 🚀 Quick Start
|
|
28
|
+
|
|
29
|
+
### 1. Wrap your suspect components
|
|
30
|
+
|
|
31
|
+
Import `PerformanceAdvisor` and wrap it around any component you want to monitor. Give it a unique `id`.
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { PerformanceAdvisor } from 'react-performance-advisor';
|
|
35
|
+
import { MyHeavyComponent } from './MyHeavyComponent';
|
|
36
|
+
|
|
37
|
+
function App() {
|
|
38
|
+
return (
|
|
39
|
+
<div>
|
|
40
|
+
<h1>My Dashboard</h1>
|
|
41
|
+
|
|
42
|
+
{/* Wrap the component you want to monitor */}
|
|
43
|
+
<PerformanceAdvisor id="HeavyDashboard">
|
|
44
|
+
<MyHeavyComponent data={someData} />
|
|
45
|
+
</PerformanceAdvisor>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2. Add the AI Panel
|
|
52
|
+
|
|
53
|
+
Drop the `PerformanceAdvisorPanel` anywhere in your top-level `App.tsx` file.
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
import { PerformanceAdvisorPanel } from 'react-performance-advisor';
|
|
57
|
+
|
|
58
|
+
function App() {
|
|
59
|
+
return (
|
|
60
|
+
<div>
|
|
61
|
+
{/* ... your app code ... */}
|
|
62
|
+
|
|
63
|
+
{/* Drop the panel at the bottom of your app */}
|
|
64
|
+
<PerformanceAdvisorPanel />
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 3. Diagnose!
|
|
71
|
+
|
|
72
|
+
1. Run your app in **development mode**.
|
|
73
|
+
2. Interact with your app — if a monitored component re-renders unnecessarily or lags, a warning will appear in the floating panel.
|
|
74
|
+
3. Click **"✨ Ask AI How to Fix This"**.
|
|
75
|
+
4. Enter your API key when prompted (it will be saved locally).
|
|
76
|
+
5. Copy and paste the AI's optimized code directly into your editor!
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## ⚠️ Note on React Strict Mode
|
|
81
|
+
|
|
82
|
+
If you are running React 18+ in Development Mode with `<React.StrictMode>`, React intentionally double-renders components to catch side effects. This tool will accurately report both renders.
|
|
83
|
+
|
|
84
|
+
To see true single-render metrics, temporarily disable Strict Mode in your `main.tsx` or `index.tsx`:
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
// Before
|
|
88
|
+
root.render(
|
|
89
|
+
<React.StrictMode>
|
|
90
|
+
<App />
|
|
91
|
+
</React.StrictMode>
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// After (for profiling)
|
|
95
|
+
root.render(<App />);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 🛠️ How It Works
|
|
101
|
+
|
|
102
|
+
| Step | What Happens |
|
|
103
|
+
|------|-------------|
|
|
104
|
+
| **Wrap** | `<PerformanceAdvisor>` attaches React's native `<Profiler>` API to your component |
|
|
105
|
+
| **Detect** | On every re-render, it checks render duration and performs deep prop diffing |
|
|
106
|
+
| **Report** | Issues are aggregated and surfaced in the floating `<PerformanceAdvisorPanel>` |
|
|
107
|
+
| **Fix** | One click sends the context to AI, which streams back ready-to-paste code |
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 📄 License
|
|
112
|
+
|
|
113
|
+
MIT © React Performance Advisor Contributors
|
package/package.json
CHANGED
package/dist/index.d.mts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import React from 'react';
|
|
3
|
-
|
|
4
|
-
interface PerformanceAdvisorProps {
|
|
5
|
-
id: string;
|
|
6
|
-
children: React.ReactNode;
|
|
7
|
-
}
|
|
8
|
-
declare const PerformanceAdvisor: ({ id, children, }: PerformanceAdvisorProps) => react_jsx_runtime.JSX.Element;
|
|
9
|
-
|
|
10
|
-
declare const PerformanceAdvisorPanel: () => react_jsx_runtime.JSX.Element;
|
|
11
|
-
|
|
12
|
-
export { PerformanceAdvisor, PerformanceAdvisorPanel };
|
package/dist/index.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import React from 'react';
|
|
3
|
-
|
|
4
|
-
interface PerformanceAdvisorProps {
|
|
5
|
-
id: string;
|
|
6
|
-
children: React.ReactNode;
|
|
7
|
-
}
|
|
8
|
-
declare const PerformanceAdvisor: ({ id, children, }: PerformanceAdvisorProps) => react_jsx_runtime.JSX.Element;
|
|
9
|
-
|
|
10
|
-
declare const PerformanceAdvisorPanel: () => react_jsx_runtime.JSX.Element;
|
|
11
|
-
|
|
12
|
-
export { PerformanceAdvisor, PerformanceAdvisorPanel };
|
package/dist/index.js
DELETED
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __defProps = Object.defineProperties;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
6
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
8
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
-
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
10
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
11
|
-
var __spreadValues = (a, b) => {
|
|
12
|
-
for (var prop in b || (b = {}))
|
|
13
|
-
if (__hasOwnProp.call(b, prop))
|
|
14
|
-
__defNormalProp(a, prop, b[prop]);
|
|
15
|
-
if (__getOwnPropSymbols)
|
|
16
|
-
for (var prop of __getOwnPropSymbols(b)) {
|
|
17
|
-
if (__propIsEnum.call(b, prop))
|
|
18
|
-
__defNormalProp(a, prop, b[prop]);
|
|
19
|
-
}
|
|
20
|
-
return a;
|
|
21
|
-
};
|
|
22
|
-
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
23
|
-
var __export = (target, all) => {
|
|
24
|
-
for (var name in all)
|
|
25
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
26
|
-
};
|
|
27
|
-
var __copyProps = (to, from, except, desc) => {
|
|
28
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
29
|
-
for (let key of __getOwnPropNames(from))
|
|
30
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
31
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
32
|
-
}
|
|
33
|
-
return to;
|
|
34
|
-
};
|
|
35
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
36
|
-
|
|
37
|
-
// src/index.ts
|
|
38
|
-
var index_exports = {};
|
|
39
|
-
__export(index_exports, {
|
|
40
|
-
PerformanceAdvisor: () => PerformanceAdvisor,
|
|
41
|
-
PerformanceAdvisorPanel: () => PerformanceAdvisorPanel
|
|
42
|
-
});
|
|
43
|
-
module.exports = __toCommonJS(index_exports);
|
|
44
|
-
|
|
45
|
-
// src/PerformanceAdvisor.tsx
|
|
46
|
-
var import_react2 = require("react");
|
|
47
|
-
|
|
48
|
-
// src/useRenderTracker.ts
|
|
49
|
-
var import_react = require("react");
|
|
50
|
-
|
|
51
|
-
// src/store.ts
|
|
52
|
-
var issues = [];
|
|
53
|
-
var listeners = /* @__PURE__ */ new Set();
|
|
54
|
-
var performanceStore = {
|
|
55
|
-
// 1. Add a new issue to the top of the list (keep max 50 to avoid memory leaks)
|
|
56
|
-
addIssue: (issue) => {
|
|
57
|
-
issues = [__spreadProps(__spreadValues({}, issue), { timestamp: Date.now() }), ...issues].slice(0, 50);
|
|
58
|
-
listeners.forEach((listener) => listener());
|
|
59
|
-
},
|
|
60
|
-
// 2. Get all current issues
|
|
61
|
-
getIssues: () => issues,
|
|
62
|
-
// 3. Clear the dashboard
|
|
63
|
-
clear: () => {
|
|
64
|
-
issues = [];
|
|
65
|
-
listeners.forEach((listener) => listener());
|
|
66
|
-
},
|
|
67
|
-
// 4. Let the UI Panel subscribe to changes
|
|
68
|
-
subscribe: (listener) => {
|
|
69
|
-
listeners.add(listener);
|
|
70
|
-
return () => listeners.delete(listener);
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
// src/useRenderTracker.ts
|
|
75
|
-
function useRenderTracker(componentId, currentProps) {
|
|
76
|
-
const prevProps = (0, import_react.useRef)({});
|
|
77
|
-
const renderCount = (0, import_react.useRef)(0);
|
|
78
|
-
renderCount.current += 1;
|
|
79
|
-
(0, import_react.useEffect)(() => {
|
|
80
|
-
const changedProps = [];
|
|
81
|
-
if (prevProps.current) {
|
|
82
|
-
Object.keys(__spreadValues(__spreadValues({}, prevProps.current), currentProps)).forEach((key) => {
|
|
83
|
-
if (prevProps.current[key] !== currentProps[key]) {
|
|
84
|
-
changedProps.push({
|
|
85
|
-
propName: key,
|
|
86
|
-
prev: prevProps.current[key],
|
|
87
|
-
next: currentProps[key]
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
if (changedProps.length > 0 && renderCount.current > 1) {
|
|
93
|
-
performanceStore.addIssue({
|
|
94
|
-
id: componentId,
|
|
95
|
-
renderCount: renderCount.current,
|
|
96
|
-
changedProps
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
prevProps.current = currentProps;
|
|
100
|
-
});
|
|
101
|
-
return renderCount.current;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// src/PerformanceAdvisor.tsx
|
|
105
|
-
var import_jsx_runtime = require("react/jsx-runtime");
|
|
106
|
-
var PerformanceAdvisor = ({
|
|
107
|
-
id,
|
|
108
|
-
children
|
|
109
|
-
}) => {
|
|
110
|
-
const childProps = (0, import_react2.isValidElement)(children) ? children.props : {};
|
|
111
|
-
const renderCount = useRenderTracker(id, childProps);
|
|
112
|
-
const handleRender = (profilerId, phase, actualDuration) => {
|
|
113
|
-
if (phase === "update") {
|
|
114
|
-
console.log(
|
|
115
|
-
`[Profiler: ${profilerId}] Render #${renderCount} took ${actualDuration.toFixed(2)}ms`
|
|
116
|
-
);
|
|
117
|
-
const issues2 = performanceStore.getIssues();
|
|
118
|
-
const currentIssue = issues2.find(
|
|
119
|
-
(i) => i.id === profilerId && i.renderCount === renderCount
|
|
120
|
-
);
|
|
121
|
-
if (currentIssue) {
|
|
122
|
-
currentIssue.duration = actualDuration;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
const ProfilerComponent = import_react2.Profiler;
|
|
127
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ProfilerComponent, { id, onRender: handleRender, children });
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
// src/PerformanceAdvisorPanel.tsx
|
|
131
|
-
var import_react3 = require("react");
|
|
132
|
-
var import_generative_ai = require("@google/generative-ai");
|
|
133
|
-
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
134
|
-
var CodeBlock = ({ code }) => {
|
|
135
|
-
const [copied, setCopied] = (0, import_react3.useState)(false);
|
|
136
|
-
const handleCopy = async () => {
|
|
137
|
-
try {
|
|
138
|
-
await navigator.clipboard.writeText(code);
|
|
139
|
-
setCopied(true);
|
|
140
|
-
setTimeout(() => setCopied(false), 2e3);
|
|
141
|
-
} catch (err) {
|
|
142
|
-
console.error("Failed to copy code", err);
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { position: "relative", background: "#1e1e1e", color: "#d4d4d4", padding: "12px", borderRadius: "6px", overflowX: "auto", fontFamily: "monospace", marginTop: "8px", marginBottom: "8px", border: "1px solid #333" }, children: [
|
|
146
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
147
|
-
"button",
|
|
148
|
-
{
|
|
149
|
-
onClick: handleCopy,
|
|
150
|
-
style: { position: "absolute", top: "8px", right: "8px", background: "#333", color: "#fff", border: "1px solid #555", borderRadius: "4px", padding: "4px 8px", cursor: "pointer", fontSize: "11px", transition: "0.2s" },
|
|
151
|
-
children: copied ? "\u2705 Copied!" : "\u{1F4CB} Copy"
|
|
152
|
-
}
|
|
153
|
-
),
|
|
154
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("pre", { style: { margin: 0, marginTop: "20px" }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("code", { children: code }) })
|
|
155
|
-
] });
|
|
156
|
-
};
|
|
157
|
-
var PerformanceAdvisorPanel = () => {
|
|
158
|
-
const [issues2, setIssues] = (0, import_react3.useState)([]);
|
|
159
|
-
const [isOpen, setIsOpen] = (0, import_react3.useState)(true);
|
|
160
|
-
const [loadingAi, setLoadingAi] = (0, import_react3.useState)(null);
|
|
161
|
-
const [aiResponses, setAiResponses] = (0, import_react3.useState)({});
|
|
162
|
-
const [apiKey, setApiKey] = (0, import_react3.useState)(() => localStorage.getItem("rpa_gemini_key") || "");
|
|
163
|
-
const [keyInput, setKeyInput] = (0, import_react3.useState)("");
|
|
164
|
-
const [keyError, setKeyError] = (0, import_react3.useState)(false);
|
|
165
|
-
(0, import_react3.useEffect)(() => {
|
|
166
|
-
const unsubscribe = performanceStore.subscribe(() => {
|
|
167
|
-
setIssues([...performanceStore.getIssues()]);
|
|
168
|
-
});
|
|
169
|
-
return () => {
|
|
170
|
-
unsubscribe();
|
|
171
|
-
};
|
|
172
|
-
}, []);
|
|
173
|
-
const groupedIssues = (0, import_react3.useMemo)(() => {
|
|
174
|
-
const groups = {};
|
|
175
|
-
issues2.forEach((issue) => {
|
|
176
|
-
const signature = issue.changedProps.map((p) => p.propName).sort().join(",");
|
|
177
|
-
if (!groups[signature]) {
|
|
178
|
-
groups[signature] = __spreadProps(__spreadValues({}, issue), {
|
|
179
|
-
affectedCount: 1,
|
|
180
|
-
totalDuration: issue.duration || 0,
|
|
181
|
-
baseId: issue.id
|
|
182
|
-
});
|
|
183
|
-
} else {
|
|
184
|
-
groups[signature].affectedCount += 1;
|
|
185
|
-
groups[signature].totalDuration += issue.duration || 0;
|
|
186
|
-
groups[signature].renderCount = Math.max(groups[signature].renderCount, issue.renderCount);
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
return Object.values(groups).map((group) => {
|
|
190
|
-
if (group.affectedCount > 1) {
|
|
191
|
-
group.id = `${group.baseId} (+ ${group.affectedCount - 1} identical components)`;
|
|
192
|
-
group.duration = group.totalDuration;
|
|
193
|
-
}
|
|
194
|
-
return group;
|
|
195
|
-
});
|
|
196
|
-
}, [issues2]);
|
|
197
|
-
const askGemini = async (issue, index) => {
|
|
198
|
-
if (!apiKey) {
|
|
199
|
-
setKeyError(true);
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
setKeyError(false);
|
|
203
|
-
const issueKey = `${issue.id}-${index}`;
|
|
204
|
-
setLoadingAi(issueKey);
|
|
205
|
-
try {
|
|
206
|
-
const genAI = new import_generative_ai.GoogleGenerativeAI(apiKey);
|
|
207
|
-
const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" });
|
|
208
|
-
const changedPropsList = issue.changedProps.map((p) => p.propName).join(", ");
|
|
209
|
-
const prompt = `
|
|
210
|
-
You are an expert React performance consultant.
|
|
211
|
-
A React component named '<${issue.id}>' just re-rendered ${issue.renderCount} times.
|
|
212
|
-
${issue.duration ? `CRITICAL: The collective render time took a massive ${issue.duration.toFixed(2)} milliseconds, which causes UI lag.` : ""}
|
|
213
|
-
The props that changed are: ${changedPropsList}.
|
|
214
|
-
|
|
215
|
-
If the render duration is high (>16ms), explain why heavy synchronous tasks block the main thread and suggest moving the calculation to a useMemo, a Web Worker, or useEffect. Give a brief explanation and a short code fix.
|
|
216
|
-
`;
|
|
217
|
-
const result = await model.generateContent(prompt);
|
|
218
|
-
setAiResponses((prev) => __spreadProps(__spreadValues({}, prev), {
|
|
219
|
-
[issueKey]: result.response.text()
|
|
220
|
-
}));
|
|
221
|
-
} catch (error) {
|
|
222
|
-
console.error("AI Error:", error);
|
|
223
|
-
if (error instanceof Error && error.message.includes("API key")) {
|
|
224
|
-
localStorage.removeItem("rpa_gemini_key");
|
|
225
|
-
setApiKey("");
|
|
226
|
-
}
|
|
227
|
-
setAiResponses((prev) => __spreadProps(__spreadValues({}, prev), {
|
|
228
|
-
[issueKey]: "Failed to connect to AI. Check console, or your API key might be invalid."
|
|
229
|
-
}));
|
|
230
|
-
} finally {
|
|
231
|
-
setLoadingAi(null);
|
|
232
|
-
}
|
|
233
|
-
};
|
|
234
|
-
const renderFormattedText = (text) => {
|
|
235
|
-
return text.split("```").map((part, index) => {
|
|
236
|
-
if (index % 2 === 1) {
|
|
237
|
-
const cleanCode = part.replace(/^[\w-]+\n/, "");
|
|
238
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CodeBlock, { code: cleanCode }, index);
|
|
239
|
-
}
|
|
240
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: part }, index);
|
|
241
|
-
});
|
|
242
|
-
};
|
|
243
|
-
const handleSaveKey = () => {
|
|
244
|
-
if (keyInput.trim()) {
|
|
245
|
-
localStorage.setItem("rpa_gemini_key", keyInput.trim());
|
|
246
|
-
setApiKey(keyInput.trim());
|
|
247
|
-
setKeyError(false);
|
|
248
|
-
setKeyInput("");
|
|
249
|
-
}
|
|
250
|
-
};
|
|
251
|
-
if (!isOpen) {
|
|
252
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
253
|
-
"button",
|
|
254
|
-
{
|
|
255
|
-
onClick: () => setIsOpen(true),
|
|
256
|
-
style: { position: "fixed", bottom: 20, right: 20, background: "#333", color: "#fff", padding: "10px", borderRadius: "8px", zIndex: 9999 },
|
|
257
|
-
children: [
|
|
258
|
-
"\u{1F50D} Open Advisor (",
|
|
259
|
-
groupedIssues.length,
|
|
260
|
-
")"
|
|
261
|
-
]
|
|
262
|
-
}
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { position: "fixed", bottom: 20, right: 20, width: "450px", background: "#fff", border: "1px solid #ccc", borderRadius: "8px", boxShadow: "0 4px 12px rgba(0,0,0,0.15)", zIndex: 9999, fontFamily: "sans-serif", color: "#333", display: "flex", flexDirection: "column" }, children: [
|
|
266
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { background: "#333", color: "#fff", padding: "10px", borderTopLeftRadius: "8px", borderTopRightRadius: "8px", display: "flex", justifyContent: "space-between" }, children: [
|
|
267
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { children: "\u{1F9E0} Performance Advisor" }),
|
|
268
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { onClick: () => setIsOpen(false), style: { background: "transparent", border: "none", color: "#fff", cursor: "pointer" }, children: "\u2716" })
|
|
269
|
-
] }),
|
|
270
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { maxHeight: "400px", overflowY: "auto", padding: "10px", flexGrow: 1 }, children: groupedIssues.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { color: "#666", textAlign: "center" }, children: "No issues detected yet. Click around your app!" }) : groupedIssues.map((issue, index) => {
|
|
271
|
-
const issueKey = `${issue.id}-${index}`;
|
|
272
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { borderBottom: "1px solid #eee", paddingBottom: "10px", marginBottom: "10px" }, children: [
|
|
273
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { color: "#d9534f", fontWeight: "bold", fontSize: "14px" }, children: [
|
|
274
|
-
"\u26A0\uFE0F ",
|
|
275
|
-
issue.id
|
|
276
|
-
] }),
|
|
277
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { fontSize: "13px", marginTop: "5px", color: "#333" }, children: [
|
|
278
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { children: "Changed Props:" }),
|
|
279
|
-
" ",
|
|
280
|
-
issue.changedProps.map((p) => p.propName).join(", "),
|
|
281
|
-
issue.duration && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { color: "#e65100", fontWeight: "bold", marginLeft: "8px" }, children: [
|
|
282
|
-
"\u23F1\uFE0F ",
|
|
283
|
-
issue.duration.toFixed(1),
|
|
284
|
-
"ms Lag"
|
|
285
|
-
] })
|
|
286
|
-
] }),
|
|
287
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { marginTop: "10px" }, children: [
|
|
288
|
-
!aiResponses[issueKey] && loadingAi !== issueKey && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { onClick: () => askGemini(issue, index), style: { background: "#e1f5fe", border: "1px solid #0288d1", color: "#0288d1", padding: "6px 12px", borderRadius: "4px", cursor: "pointer", fontSize: "12px", fontWeight: "bold" }, children: "\u2728 Ask AI How to Fix This" }),
|
|
289
|
-
loadingAi === issueKey && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { fontSize: "12px", color: "#666" }, children: "\u{1F9E0} AI is diagnosing..." }),
|
|
290
|
-
aiResponses[issueKey] && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { background: "#f0f4f8", color: "#1e293b", padding: "12px", borderRadius: "6px", marginTop: "8px", fontSize: "13px", lineHeight: "1.5", borderLeft: "4px solid #0288d1", whiteSpace: "pre-wrap", boxShadow: "inset 0 1px 3px rgba(0,0,0,0.05)" }, children: [
|
|
291
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { style: { color: "#0288d1", display: "block", marginBottom: "4px" }, children: "\u2728 AI Suggestion:" }),
|
|
292
|
-
renderFormattedText(aiResponses[issueKey])
|
|
293
|
-
] })
|
|
294
|
-
] })
|
|
295
|
-
] }, index);
|
|
296
|
-
}) }),
|
|
297
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { padding: "10px", borderTop: "1px solid #eee", background: "#fafafa", borderBottomLeftRadius: "8px", borderBottomRightRadius: "8px" }, children: !apiKey ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
|
|
298
|
-
keyError && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { color: "red", fontSize: "12px", margin: "0 0 5px 0" }, children: "\u26A0\uFE0F Please save your API key first!" }),
|
|
299
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
|
|
300
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
301
|
-
"input",
|
|
302
|
-
{
|
|
303
|
-
type: "password",
|
|
304
|
-
placeholder: "Enter Gemini API Key...",
|
|
305
|
-
value: keyInput,
|
|
306
|
-
onChange: (e) => setKeyInput(e.target.value),
|
|
307
|
-
style: { flexGrow: 1, padding: "6px", borderRadius: "4px", border: "1px solid #ccc" }
|
|
308
|
-
}
|
|
309
|
-
),
|
|
310
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { onClick: handleSaveKey, style: { background: "#0288d1", color: "#fff", border: "none", padding: "6px 12px", borderRadius: "4px", cursor: "pointer", fontWeight: "bold" }, children: "Save" })
|
|
311
|
-
] }),
|
|
312
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { fontSize: "11px", color: "#666", margin: "5px 0 0 0" }, children: "Key is saved securely in your browser's Local Storage." })
|
|
313
|
-
] }) : (
|
|
314
|
-
/* If key exists, show normal controls */
|
|
315
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
|
|
316
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { onClick: () => performanceStore.clear(), style: { padding: "5px 10px", cursor: "pointer", color: "#333", background: "#e0e0e0", border: "1px solid #ccc", borderRadius: "4px" }, children: "Clear Logs" }),
|
|
317
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { onClick: () => {
|
|
318
|
-
localStorage.removeItem("rpa_gemini_key");
|
|
319
|
-
setApiKey("");
|
|
320
|
-
}, style: { fontSize: "11px", background: "none", border: "none", color: "#888", cursor: "pointer", textDecoration: "underline" }, children: "Remove API Key" })
|
|
321
|
-
] })
|
|
322
|
-
) })
|
|
323
|
-
] });
|
|
324
|
-
};
|
|
325
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
326
|
-
0 && (module.exports = {
|
|
327
|
-
PerformanceAdvisor,
|
|
328
|
-
PerformanceAdvisorPanel
|
|
329
|
-
});
|
package/dist/index.mjs
DELETED
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __defProps = Object.defineProperties;
|
|
3
|
-
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
4
|
-
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
7
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
8
|
-
var __spreadValues = (a, b) => {
|
|
9
|
-
for (var prop in b || (b = {}))
|
|
10
|
-
if (__hasOwnProp.call(b, prop))
|
|
11
|
-
__defNormalProp(a, prop, b[prop]);
|
|
12
|
-
if (__getOwnPropSymbols)
|
|
13
|
-
for (var prop of __getOwnPropSymbols(b)) {
|
|
14
|
-
if (__propIsEnum.call(b, prop))
|
|
15
|
-
__defNormalProp(a, prop, b[prop]);
|
|
16
|
-
}
|
|
17
|
-
return a;
|
|
18
|
-
};
|
|
19
|
-
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
20
|
-
|
|
21
|
-
// src/PerformanceAdvisor.tsx
|
|
22
|
-
import {
|
|
23
|
-
Profiler,
|
|
24
|
-
isValidElement
|
|
25
|
-
} from "react";
|
|
26
|
-
|
|
27
|
-
// src/useRenderTracker.ts
|
|
28
|
-
import { useRef, useEffect } from "react";
|
|
29
|
-
|
|
30
|
-
// src/store.ts
|
|
31
|
-
var issues = [];
|
|
32
|
-
var listeners = /* @__PURE__ */ new Set();
|
|
33
|
-
var performanceStore = {
|
|
34
|
-
// 1. Add a new issue to the top of the list (keep max 50 to avoid memory leaks)
|
|
35
|
-
addIssue: (issue) => {
|
|
36
|
-
issues = [__spreadProps(__spreadValues({}, issue), { timestamp: Date.now() }), ...issues].slice(0, 50);
|
|
37
|
-
listeners.forEach((listener) => listener());
|
|
38
|
-
},
|
|
39
|
-
// 2. Get all current issues
|
|
40
|
-
getIssues: () => issues,
|
|
41
|
-
// 3. Clear the dashboard
|
|
42
|
-
clear: () => {
|
|
43
|
-
issues = [];
|
|
44
|
-
listeners.forEach((listener) => listener());
|
|
45
|
-
},
|
|
46
|
-
// 4. Let the UI Panel subscribe to changes
|
|
47
|
-
subscribe: (listener) => {
|
|
48
|
-
listeners.add(listener);
|
|
49
|
-
return () => listeners.delete(listener);
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// src/useRenderTracker.ts
|
|
54
|
-
function useRenderTracker(componentId, currentProps) {
|
|
55
|
-
const prevProps = useRef({});
|
|
56
|
-
const renderCount = useRef(0);
|
|
57
|
-
renderCount.current += 1;
|
|
58
|
-
useEffect(() => {
|
|
59
|
-
const changedProps = [];
|
|
60
|
-
if (prevProps.current) {
|
|
61
|
-
Object.keys(__spreadValues(__spreadValues({}, prevProps.current), currentProps)).forEach((key) => {
|
|
62
|
-
if (prevProps.current[key] !== currentProps[key]) {
|
|
63
|
-
changedProps.push({
|
|
64
|
-
propName: key,
|
|
65
|
-
prev: prevProps.current[key],
|
|
66
|
-
next: currentProps[key]
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
if (changedProps.length > 0 && renderCount.current > 1) {
|
|
72
|
-
performanceStore.addIssue({
|
|
73
|
-
id: componentId,
|
|
74
|
-
renderCount: renderCount.current,
|
|
75
|
-
changedProps
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
prevProps.current = currentProps;
|
|
79
|
-
});
|
|
80
|
-
return renderCount.current;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// src/PerformanceAdvisor.tsx
|
|
84
|
-
import { jsx } from "react/jsx-runtime";
|
|
85
|
-
var PerformanceAdvisor = ({
|
|
86
|
-
id,
|
|
87
|
-
children
|
|
88
|
-
}) => {
|
|
89
|
-
const childProps = isValidElement(children) ? children.props : {};
|
|
90
|
-
const renderCount = useRenderTracker(id, childProps);
|
|
91
|
-
const handleRender = (profilerId, phase, actualDuration) => {
|
|
92
|
-
if (phase === "update") {
|
|
93
|
-
console.log(
|
|
94
|
-
`[Profiler: ${profilerId}] Render #${renderCount} took ${actualDuration.toFixed(2)}ms`
|
|
95
|
-
);
|
|
96
|
-
const issues2 = performanceStore.getIssues();
|
|
97
|
-
const currentIssue = issues2.find(
|
|
98
|
-
(i) => i.id === profilerId && i.renderCount === renderCount
|
|
99
|
-
);
|
|
100
|
-
if (currentIssue) {
|
|
101
|
-
currentIssue.duration = actualDuration;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
const ProfilerComponent = Profiler;
|
|
106
|
-
return /* @__PURE__ */ jsx(ProfilerComponent, { id, onRender: handleRender, children });
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
// src/PerformanceAdvisorPanel.tsx
|
|
110
|
-
import { useEffect as useEffect2, useState, useMemo } from "react";
|
|
111
|
-
import { GoogleGenerativeAI } from "@google/generative-ai";
|
|
112
|
-
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
113
|
-
var CodeBlock = ({ code }) => {
|
|
114
|
-
const [copied, setCopied] = useState(false);
|
|
115
|
-
const handleCopy = async () => {
|
|
116
|
-
try {
|
|
117
|
-
await navigator.clipboard.writeText(code);
|
|
118
|
-
setCopied(true);
|
|
119
|
-
setTimeout(() => setCopied(false), 2e3);
|
|
120
|
-
} catch (err) {
|
|
121
|
-
console.error("Failed to copy code", err);
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
return /* @__PURE__ */ jsxs("div", { style: { position: "relative", background: "#1e1e1e", color: "#d4d4d4", padding: "12px", borderRadius: "6px", overflowX: "auto", fontFamily: "monospace", marginTop: "8px", marginBottom: "8px", border: "1px solid #333" }, children: [
|
|
125
|
-
/* @__PURE__ */ jsx2(
|
|
126
|
-
"button",
|
|
127
|
-
{
|
|
128
|
-
onClick: handleCopy,
|
|
129
|
-
style: { position: "absolute", top: "8px", right: "8px", background: "#333", color: "#fff", border: "1px solid #555", borderRadius: "4px", padding: "4px 8px", cursor: "pointer", fontSize: "11px", transition: "0.2s" },
|
|
130
|
-
children: copied ? "\u2705 Copied!" : "\u{1F4CB} Copy"
|
|
131
|
-
}
|
|
132
|
-
),
|
|
133
|
-
/* @__PURE__ */ jsx2("pre", { style: { margin: 0, marginTop: "20px" }, children: /* @__PURE__ */ jsx2("code", { children: code }) })
|
|
134
|
-
] });
|
|
135
|
-
};
|
|
136
|
-
var PerformanceAdvisorPanel = () => {
|
|
137
|
-
const [issues2, setIssues] = useState([]);
|
|
138
|
-
const [isOpen, setIsOpen] = useState(true);
|
|
139
|
-
const [loadingAi, setLoadingAi] = useState(null);
|
|
140
|
-
const [aiResponses, setAiResponses] = useState({});
|
|
141
|
-
const [apiKey, setApiKey] = useState(() => localStorage.getItem("rpa_gemini_key") || "");
|
|
142
|
-
const [keyInput, setKeyInput] = useState("");
|
|
143
|
-
const [keyError, setKeyError] = useState(false);
|
|
144
|
-
useEffect2(() => {
|
|
145
|
-
const unsubscribe = performanceStore.subscribe(() => {
|
|
146
|
-
setIssues([...performanceStore.getIssues()]);
|
|
147
|
-
});
|
|
148
|
-
return () => {
|
|
149
|
-
unsubscribe();
|
|
150
|
-
};
|
|
151
|
-
}, []);
|
|
152
|
-
const groupedIssues = useMemo(() => {
|
|
153
|
-
const groups = {};
|
|
154
|
-
issues2.forEach((issue) => {
|
|
155
|
-
const signature = issue.changedProps.map((p) => p.propName).sort().join(",");
|
|
156
|
-
if (!groups[signature]) {
|
|
157
|
-
groups[signature] = __spreadProps(__spreadValues({}, issue), {
|
|
158
|
-
affectedCount: 1,
|
|
159
|
-
totalDuration: issue.duration || 0,
|
|
160
|
-
baseId: issue.id
|
|
161
|
-
});
|
|
162
|
-
} else {
|
|
163
|
-
groups[signature].affectedCount += 1;
|
|
164
|
-
groups[signature].totalDuration += issue.duration || 0;
|
|
165
|
-
groups[signature].renderCount = Math.max(groups[signature].renderCount, issue.renderCount);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
return Object.values(groups).map((group) => {
|
|
169
|
-
if (group.affectedCount > 1) {
|
|
170
|
-
group.id = `${group.baseId} (+ ${group.affectedCount - 1} identical components)`;
|
|
171
|
-
group.duration = group.totalDuration;
|
|
172
|
-
}
|
|
173
|
-
return group;
|
|
174
|
-
});
|
|
175
|
-
}, [issues2]);
|
|
176
|
-
const askGemini = async (issue, index) => {
|
|
177
|
-
if (!apiKey) {
|
|
178
|
-
setKeyError(true);
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
setKeyError(false);
|
|
182
|
-
const issueKey = `${issue.id}-${index}`;
|
|
183
|
-
setLoadingAi(issueKey);
|
|
184
|
-
try {
|
|
185
|
-
const genAI = new GoogleGenerativeAI(apiKey);
|
|
186
|
-
const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" });
|
|
187
|
-
const changedPropsList = issue.changedProps.map((p) => p.propName).join(", ");
|
|
188
|
-
const prompt = `
|
|
189
|
-
You are an expert React performance consultant.
|
|
190
|
-
A React component named '<${issue.id}>' just re-rendered ${issue.renderCount} times.
|
|
191
|
-
${issue.duration ? `CRITICAL: The collective render time took a massive ${issue.duration.toFixed(2)} milliseconds, which causes UI lag.` : ""}
|
|
192
|
-
The props that changed are: ${changedPropsList}.
|
|
193
|
-
|
|
194
|
-
If the render duration is high (>16ms), explain why heavy synchronous tasks block the main thread and suggest moving the calculation to a useMemo, a Web Worker, or useEffect. Give a brief explanation and a short code fix.
|
|
195
|
-
`;
|
|
196
|
-
const result = await model.generateContent(prompt);
|
|
197
|
-
setAiResponses((prev) => __spreadProps(__spreadValues({}, prev), {
|
|
198
|
-
[issueKey]: result.response.text()
|
|
199
|
-
}));
|
|
200
|
-
} catch (error) {
|
|
201
|
-
console.error("AI Error:", error);
|
|
202
|
-
if (error instanceof Error && error.message.includes("API key")) {
|
|
203
|
-
localStorage.removeItem("rpa_gemini_key");
|
|
204
|
-
setApiKey("");
|
|
205
|
-
}
|
|
206
|
-
setAiResponses((prev) => __spreadProps(__spreadValues({}, prev), {
|
|
207
|
-
[issueKey]: "Failed to connect to AI. Check console, or your API key might be invalid."
|
|
208
|
-
}));
|
|
209
|
-
} finally {
|
|
210
|
-
setLoadingAi(null);
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
const renderFormattedText = (text) => {
|
|
214
|
-
return text.split("```").map((part, index) => {
|
|
215
|
-
if (index % 2 === 1) {
|
|
216
|
-
const cleanCode = part.replace(/^[\w-]+\n/, "");
|
|
217
|
-
return /* @__PURE__ */ jsx2(CodeBlock, { code: cleanCode }, index);
|
|
218
|
-
}
|
|
219
|
-
return /* @__PURE__ */ jsx2("span", { children: part }, index);
|
|
220
|
-
});
|
|
221
|
-
};
|
|
222
|
-
const handleSaveKey = () => {
|
|
223
|
-
if (keyInput.trim()) {
|
|
224
|
-
localStorage.setItem("rpa_gemini_key", keyInput.trim());
|
|
225
|
-
setApiKey(keyInput.trim());
|
|
226
|
-
setKeyError(false);
|
|
227
|
-
setKeyInput("");
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
if (!isOpen) {
|
|
231
|
-
return /* @__PURE__ */ jsxs(
|
|
232
|
-
"button",
|
|
233
|
-
{
|
|
234
|
-
onClick: () => setIsOpen(true),
|
|
235
|
-
style: { position: "fixed", bottom: 20, right: 20, background: "#333", color: "#fff", padding: "10px", borderRadius: "8px", zIndex: 9999 },
|
|
236
|
-
children: [
|
|
237
|
-
"\u{1F50D} Open Advisor (",
|
|
238
|
-
groupedIssues.length,
|
|
239
|
-
")"
|
|
240
|
-
]
|
|
241
|
-
}
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
return /* @__PURE__ */ jsxs("div", { style: { position: "fixed", bottom: 20, right: 20, width: "450px", background: "#fff", border: "1px solid #ccc", borderRadius: "8px", boxShadow: "0 4px 12px rgba(0,0,0,0.15)", zIndex: 9999, fontFamily: "sans-serif", color: "#333", display: "flex", flexDirection: "column" }, children: [
|
|
245
|
-
/* @__PURE__ */ jsxs("div", { style: { background: "#333", color: "#fff", padding: "10px", borderTopLeftRadius: "8px", borderTopRightRadius: "8px", display: "flex", justifyContent: "space-between" }, children: [
|
|
246
|
-
/* @__PURE__ */ jsx2("strong", { children: "\u{1F9E0} Performance Advisor" }),
|
|
247
|
-
/* @__PURE__ */ jsx2("button", { onClick: () => setIsOpen(false), style: { background: "transparent", border: "none", color: "#fff", cursor: "pointer" }, children: "\u2716" })
|
|
248
|
-
] }),
|
|
249
|
-
/* @__PURE__ */ jsx2("div", { style: { maxHeight: "400px", overflowY: "auto", padding: "10px", flexGrow: 1 }, children: groupedIssues.length === 0 ? /* @__PURE__ */ jsx2("p", { style: { color: "#666", textAlign: "center" }, children: "No issues detected yet. Click around your app!" }) : groupedIssues.map((issue, index) => {
|
|
250
|
-
const issueKey = `${issue.id}-${index}`;
|
|
251
|
-
return /* @__PURE__ */ jsxs("div", { style: { borderBottom: "1px solid #eee", paddingBottom: "10px", marginBottom: "10px" }, children: [
|
|
252
|
-
/* @__PURE__ */ jsxs("div", { style: { color: "#d9534f", fontWeight: "bold", fontSize: "14px" }, children: [
|
|
253
|
-
"\u26A0\uFE0F ",
|
|
254
|
-
issue.id
|
|
255
|
-
] }),
|
|
256
|
-
/* @__PURE__ */ jsxs("div", { style: { fontSize: "13px", marginTop: "5px", color: "#333" }, children: [
|
|
257
|
-
/* @__PURE__ */ jsx2("strong", { children: "Changed Props:" }),
|
|
258
|
-
" ",
|
|
259
|
-
issue.changedProps.map((p) => p.propName).join(", "),
|
|
260
|
-
issue.duration && /* @__PURE__ */ jsxs("span", { style: { color: "#e65100", fontWeight: "bold", marginLeft: "8px" }, children: [
|
|
261
|
-
"\u23F1\uFE0F ",
|
|
262
|
-
issue.duration.toFixed(1),
|
|
263
|
-
"ms Lag"
|
|
264
|
-
] })
|
|
265
|
-
] }),
|
|
266
|
-
/* @__PURE__ */ jsxs("div", { style: { marginTop: "10px" }, children: [
|
|
267
|
-
!aiResponses[issueKey] && loadingAi !== issueKey && /* @__PURE__ */ jsx2("button", { onClick: () => askGemini(issue, index), style: { background: "#e1f5fe", border: "1px solid #0288d1", color: "#0288d1", padding: "6px 12px", borderRadius: "4px", cursor: "pointer", fontSize: "12px", fontWeight: "bold" }, children: "\u2728 Ask AI How to Fix This" }),
|
|
268
|
-
loadingAi === issueKey && /* @__PURE__ */ jsx2("span", { style: { fontSize: "12px", color: "#666" }, children: "\u{1F9E0} AI is diagnosing..." }),
|
|
269
|
-
aiResponses[issueKey] && /* @__PURE__ */ jsxs("div", { style: { background: "#f0f4f8", color: "#1e293b", padding: "12px", borderRadius: "6px", marginTop: "8px", fontSize: "13px", lineHeight: "1.5", borderLeft: "4px solid #0288d1", whiteSpace: "pre-wrap", boxShadow: "inset 0 1px 3px rgba(0,0,0,0.05)" }, children: [
|
|
270
|
-
/* @__PURE__ */ jsx2("strong", { style: { color: "#0288d1", display: "block", marginBottom: "4px" }, children: "\u2728 AI Suggestion:" }),
|
|
271
|
-
renderFormattedText(aiResponses[issueKey])
|
|
272
|
-
] })
|
|
273
|
-
] })
|
|
274
|
-
] }, index);
|
|
275
|
-
}) }),
|
|
276
|
-
/* @__PURE__ */ jsx2("div", { style: { padding: "10px", borderTop: "1px solid #eee", background: "#fafafa", borderBottomLeftRadius: "8px", borderBottomRightRadius: "8px" }, children: !apiKey ? /* @__PURE__ */ jsxs("div", { children: [
|
|
277
|
-
keyError && /* @__PURE__ */ jsx2("p", { style: { color: "red", fontSize: "12px", margin: "0 0 5px 0" }, children: "\u26A0\uFE0F Please save your API key first!" }),
|
|
278
|
-
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "8px" }, children: [
|
|
279
|
-
/* @__PURE__ */ jsx2(
|
|
280
|
-
"input",
|
|
281
|
-
{
|
|
282
|
-
type: "password",
|
|
283
|
-
placeholder: "Enter Gemini API Key...",
|
|
284
|
-
value: keyInput,
|
|
285
|
-
onChange: (e) => setKeyInput(e.target.value),
|
|
286
|
-
style: { flexGrow: 1, padding: "6px", borderRadius: "4px", border: "1px solid #ccc" }
|
|
287
|
-
}
|
|
288
|
-
),
|
|
289
|
-
/* @__PURE__ */ jsx2("button", { onClick: handleSaveKey, style: { background: "#0288d1", color: "#fff", border: "none", padding: "6px 12px", borderRadius: "4px", cursor: "pointer", fontWeight: "bold" }, children: "Save" })
|
|
290
|
-
] }),
|
|
291
|
-
/* @__PURE__ */ jsx2("p", { style: { fontSize: "11px", color: "#666", margin: "5px 0 0 0" }, children: "Key is saved securely in your browser's Local Storage." })
|
|
292
|
-
] }) : (
|
|
293
|
-
/* If key exists, show normal controls */
|
|
294
|
-
/* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
|
|
295
|
-
/* @__PURE__ */ jsx2("button", { onClick: () => performanceStore.clear(), style: { padding: "5px 10px", cursor: "pointer", color: "#333", background: "#e0e0e0", border: "1px solid #ccc", borderRadius: "4px" }, children: "Clear Logs" }),
|
|
296
|
-
/* @__PURE__ */ jsx2("button", { onClick: () => {
|
|
297
|
-
localStorage.removeItem("rpa_gemini_key");
|
|
298
|
-
setApiKey("");
|
|
299
|
-
}, style: { fontSize: "11px", background: "none", border: "none", color: "#888", cursor: "pointer", textDecoration: "underline" }, children: "Remove API Key" })
|
|
300
|
-
] })
|
|
301
|
-
) })
|
|
302
|
-
] });
|
|
303
|
-
};
|
|
304
|
-
export {
|
|
305
|
-
PerformanceAdvisor,
|
|
306
|
-
PerformanceAdvisorPanel
|
|
307
|
-
};
|
package/readme.md
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
## 🚀 React Performance Advisor
|
|
2
|
-
|
|
3
|
-
An enterprise-grade React performance monitoring tool powered by AI.
|
|
4
|
-
|
|
5
|
-
React Performance Advisor acts as an "X-ray machine" for your React applications. It hooks directly into React's native rendering engine to catch unnecessary re-renders, prop instability, and main-thread blocking, and then uses Gemini AI to instantly write the code to fix them.
|
|
6
|
-
|
|
7
|
-
## ✨ Features
|
|
8
|
-
|
|
9
|
-
* 🧠 AI-Powered Diagnosis:** Automatically streams context (render counts, lag time, changed props) to Gemini AI to generate exact `useMemo`, `useCallback`, or `React.memo` code fixes.
|
|
10
|
-
* ⏱️ Millisecond Lag Tracking:** Utilizes React's native `<Profiler>` to catch components taking >16ms to render, identifying main-thread blocking.
|
|
11
|
-
* 🔍 Deep Prop Tracking:** Performs strict equality checks to catch "Object/Function Reference Traps" causing silent cascading re-renders.
|
|
12
|
-
* 📦 Smart Aggregation:** Groups identical bugs across massive lists/grids into a single, clean UI warning to prevent log spam.
|
|
13
|
-
* 🔒 Secure & Local:** Your Gemini API key is stored securely in your browser's Local Storage. It never touches your source code.
|
|
14
|
-
|
|
15
|
-
## 📦 Installation
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npm install react-performance-advisor
|
|
19
|
-
```
|
|
20
|
-
## 🎮 Live Demo
|
|
21
|
-
|
|
22
|
-
Try the live interactive demo right in your browser:
|
|
23
|
-
[](https://codesandbox.io/p/sandbox/gallant-stonebraker-6q98z5)
|
|
24
|
-
|
|
25
|
-
## 🛠️ Quick Start
|
|
26
|
-
|
|
27
|
-
### 1\. Wrap your suspect components
|
|
28
|
-
|
|
29
|
-
Import `PerformanceAdvisor` and wrap it around any component you want to monitor. Give it a unique `id`.
|
|
30
|
-
|
|
31
|
-
TypeScript
|
|
32
|
-
|
|
33
|
-
import { PerformanceAdvisor } from 'react-performance-advisor';
|
|
34
|
-
import { MyHeavyComponent } from './MyHeavyComponent';
|
|
35
|
-
|
|
36
|
-
function App() {
|
|
37
|
-
return (
|
|
38
|
-
<div>
|
|
39
|
-
<h1>My Dashboard</h1>
|
|
40
|
-
|
|
41
|
-
{/* 1. Wrap the component you want to monitor */}
|
|
42
|
-
<PerformanceAdvisor id="HeavyDashboard">
|
|
43
|
-
<MyHeavyComponent data={someData} />
|
|
44
|
-
</PerformanceAdvisor>
|
|
45
|
-
|
|
46
|
-
</div>
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
### 2\. Add the AI Panel
|
|
51
|
-
|
|
52
|
-
Drop the `PerformanceAdvisorPanel` anywhere in your top-level `App.tsx` file.
|
|
53
|
-
|
|
54
|
-
TypeScript
|
|
55
|
-
|
|
56
|
-
import { PerformanceAdvisorPanel } from 'react-performance-advisor';
|
|
57
|
-
|
|
58
|
-
function App() {
|
|
59
|
-
return (
|
|
60
|
-
<div>
|
|
61
|
-
{/* ... your app code ... */}
|
|
62
|
-
|
|
63
|
-
{/* 2. Drop the panel at the bottom of your app */}
|
|
64
|
-
<PerformanceAdvisorPanel />
|
|
65
|
-
</div>
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
### 3\. Diagnose!
|
|
70
|
-
|
|
71
|
-
1. Run your app in development mode.
|
|
72
|
-
2. Interact with your app. If a monitored component re-renders unnecessarily or lags, a warning will pop up in the floating panel.
|
|
73
|
-
3. Click **"✨ Ask AI How to Fix This"**.
|
|
74
|
-
4. Enter your Gemini API Key when prompted (it will be saved locally).
|
|
75
|
-
5. Copy and paste the AI's optimized code directly into your editor!
|
|
76
|
-
|
|
77
|
-
## ⚠️ Note on React Strict Mode
|
|
78
|
-
|
|
79
|
-
If you are running React 18+ in Development Mode with `<React.StrictMode>`, React intentionally double-renders components to catch side effects. This tool will accurately report both renders. To see true single-render metrics, temporarily disable Strict Mode in your `main.tsx` or `index.tsx`.
|