@perfonext/render-mcp 0.1.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.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/format.d.ts +3 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +7 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/analysis.d.ts +17 -0
- package/dist/parser/analysis.d.ts.map +1 -0
- package/dist/parser/analysis.js +226 -0
- package/dist/parser/analysis.js.map +1 -0
- package/dist/parser/react-profile.d.ts +3 -0
- package/dist/parser/react-profile.d.ts.map +1 -0
- package/dist/parser/react-profile.js +212 -0
- package/dist/parser/react-profile.js.map +1 -0
- package/dist/parser/types.d.ts +89 -0
- package/dist/parser/types.d.ts.map +1 -0
- package/dist/parser/types.js +2 -0
- package/dist/parser/types.js.map +1 -0
- package/dist/store.d.ts +11 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +17 -0
- package/dist/store.js.map +1 -0
- package/dist/tools/get-hot-commits.d.ts +3 -0
- package/dist/tools/get-hot-commits.d.ts.map +1 -0
- package/dist/tools/get-hot-commits.js +42 -0
- package/dist/tools/get-hot-commits.js.map +1 -0
- package/dist/tools/get-render-summary.d.ts +3 -0
- package/dist/tools/get-render-summary.d.ts.map +1 -0
- package/dist/tools/get-render-summary.js +61 -0
- package/dist/tools/get-render-summary.js.map +1 -0
- package/dist/tools/get-rerender-causes.d.ts +3 -0
- package/dist/tools/get-rerender-causes.d.ts.map +1 -0
- package/dist/tools/get-rerender-causes.js +50 -0
- package/dist/tools/get-rerender-causes.js.map +1 -0
- package/dist/tools/get-slow-components.d.ts +3 -0
- package/dist/tools/get-slow-components.d.ts.map +1 -0
- package/dist/tools/get-slow-components.js +35 -0
- package/dist/tools/get-slow-components.js.map +1 -0
- package/dist/tools/load-render-profile.d.ts +3 -0
- package/dist/tools/load-render-profile.d.ts.map +1 -0
- package/dist/tools/load-render-profile.js +34 -0
- package/dist/tools/load-render-profile.js.map +1 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Perfonext
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# perfonext-render-mcp
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@perfonext/render-mcp)
|
|
4
|
+
|
|
5
|
+
`perfonext-render-mcp` is an MCP server for loading and analyzing React DevTools Profiler exports captured from Next.js apps. It gives GitHub Copilot and other MCP clients structured render data they can reason over instead of forcing the model to inspect raw profiler JSON.
|
|
6
|
+
|
|
7
|
+
## What It Does
|
|
8
|
+
|
|
9
|
+
- loads exported React Profiler JSON files from Next.js apps
|
|
10
|
+
- summarizes commits and the most expensive components
|
|
11
|
+
- ranks the hottest commits and shows the top components inside each spike
|
|
12
|
+
- identifies the slowest components by total render cost
|
|
13
|
+
- highlights components with repeated rerenders using deterministic heuristics, evidence signals, and confidence levels
|
|
14
|
+
- keeps the loaded profiles in memory so an MCP client can iterate without re-reading the file
|
|
15
|
+
|
|
16
|
+
## Tools
|
|
17
|
+
|
|
18
|
+
| Tool | Description |
|
|
19
|
+
|------|-------------|
|
|
20
|
+
| `load_render_profile` | Parse and load an exported React Profiler JSON file from disk |
|
|
21
|
+
| `get_render_summary` | Summarize a loaded profile, including top components by render cost and the hottest commits |
|
|
22
|
+
| `get_hot_commits` | Rank the most expensive commits and show the top components inside each spike |
|
|
23
|
+
| `get_slow_components` | Rank the slowest components by total actual render time |
|
|
24
|
+
| `get_rerender_causes` | Explain likely rerender causes using profile-derived heuristics, evidence, confidence, and a documented score |
|
|
25
|
+
|
|
26
|
+
`get_rerender_causes` is heuristic by design. It works from structural signals in the profiler export — update frequency, nested update propagation, commit spread, and self-intensive renders. When you record with **"Record why each component rendered"** enabled in React DevTools Profiler settings, the export includes richer `changeDescriptions` data; future versions of this tool will parse that field to surface exact prop/state/context diffs natively.
|
|
27
|
+
|
|
28
|
+
## Rerender Score Contract
|
|
29
|
+
|
|
30
|
+
`get_rerender_causes` returns a `score`, `scoreBand`, `confidence`, and `evidence[]` for each component.
|
|
31
|
+
|
|
32
|
+
- `score`: a `0-10` heuristic rerender-risk score
|
|
33
|
+
- `scoreBand`:
|
|
34
|
+
- `low`: `0.0-2.9`
|
|
35
|
+
- `medium`: `3.0-5.9`
|
|
36
|
+
- `high`: `6.0-10.0`
|
|
37
|
+
- `confidence`: how many independent evidence signals supported the finding
|
|
38
|
+
- `low`: one weak signal or only fallback export evidence
|
|
39
|
+
- `medium`: two independent signals
|
|
40
|
+
- `high`: three or more independent signals
|
|
41
|
+
- `evidence[]`: machine-readable signals such as repeated updates, nested-update propagation, wide commit spread, or high actual-vs-self duration ratio
|
|
42
|
+
|
|
43
|
+
Current score inputs are:
|
|
44
|
+
|
|
45
|
+
- repeated update-phase renders
|
|
46
|
+
- nested update propagation
|
|
47
|
+
- appearance across many commits
|
|
48
|
+
- high actual-vs-self duration ratio
|
|
49
|
+
|
|
50
|
+
The score is meant to help rank investigation order, not to claim exact root cause. Enable **"Record why each component rendered"** in React DevTools Profiler settings before recording to capture richer change metadata in the export.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
Run directly with `npx`:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx -y @perfonext/render-mcp
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or install globally:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm install -g @perfonext/render-mcp
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The executable command remains `perfonext-render-mcp` after installation.
|
|
67
|
+
|
|
68
|
+
## MCP Configuration
|
|
69
|
+
|
|
70
|
+
Add this server to VS Code settings:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"mcp": {
|
|
75
|
+
"servers": {
|
|
76
|
+
"perfonext-render": {
|
|
77
|
+
"command": "npx",
|
|
78
|
+
"args": ["-y", "@perfonext/render-mcp"]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Local workspace MCP config is also included in `.vscode/mcp.json` for development.
|
|
86
|
+
|
|
87
|
+
## Example Copilot Prompts
|
|
88
|
+
|
|
89
|
+
- "Load the React Profiler export at `./profile.json` and summarize the hottest components."
|
|
90
|
+
- "Show me the hottest commits in `./profile.json` and which components dominated each spike."
|
|
91
|
+
- "Which components are consuming the most render time in this Next.js profile?"
|
|
92
|
+
- "Show me the likely rerender causes for the slowest components, including evidence and confidence."
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npm install
|
|
98
|
+
npm run build
|
|
99
|
+
npm test
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Sample fixtures for local validation live under `tests/fixtures/`.
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C"}
|
package/dist/format.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,QAAQ,CAAC,KAAa;IACpC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,OAAO,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;AACxC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { registerLoadRenderProfile } from './tools/load-render-profile.js';
|
|
5
|
+
import { registerGetRenderSummary } from './tools/get-render-summary.js';
|
|
6
|
+
import { registerGetHotCommits } from './tools/get-hot-commits.js';
|
|
7
|
+
import { registerGetSlowComponents } from './tools/get-slow-components.js';
|
|
8
|
+
import { registerGetRerenderCauses } from './tools/get-rerender-causes.js';
|
|
9
|
+
const server = new McpServer({
|
|
10
|
+
name: 'perfonext-render-mcp',
|
|
11
|
+
version: '0.1.0',
|
|
12
|
+
});
|
|
13
|
+
registerLoadRenderProfile(server);
|
|
14
|
+
registerGetRenderSummary(server);
|
|
15
|
+
registerGetHotCommits(server);
|
|
16
|
+
registerGetSlowComponents(server);
|
|
17
|
+
registerGetRerenderCauses(server);
|
|
18
|
+
const transport = new StdioServerTransport();
|
|
19
|
+
await server.connect(transport);
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAC3E,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAC3E,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAE3E,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,sBAAsB;IAC5B,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH,yBAAyB,CAAC,MAAM,CAAC,CAAC;AAClC,wBAAwB,CAAC,MAAM,CAAC,CAAC;AACjC,qBAAqB,CAAC,MAAM,CAAC,CAAC;AAC9B,yBAAyB,CAAC,MAAM,CAAC,CAAC;AAClC,yBAAyB,CAAC,MAAM,CAAC,CAAC;AAElC,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;AAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { HotCommitSummary, ParsedRenderProfile, RenderSummaryEntry, RerenderCause } from './types.js';
|
|
2
|
+
export declare function getHotCommits(profile: ParsedRenderProfile, limit?: number, componentLimit?: number): HotCommitSummary[];
|
|
3
|
+
export declare function getRenderSummary(profile: ParsedRenderProfile, limit?: number): {
|
|
4
|
+
profileId: string;
|
|
5
|
+
filename: string;
|
|
6
|
+
version: string;
|
|
7
|
+
rendererId: number | null;
|
|
8
|
+
commitCount: number;
|
|
9
|
+
componentCount: number;
|
|
10
|
+
totalCommitDuration: number;
|
|
11
|
+
totalRenderDuration: number;
|
|
12
|
+
topComponents: RenderSummaryEntry[];
|
|
13
|
+
hotCommits: HotCommitSummary[];
|
|
14
|
+
};
|
|
15
|
+
export declare function getSlowComponents(profile: ParsedRenderProfile, limit?: number, sortBy?: 'total' | 'average' | 'max', minDuration?: number): RenderSummaryEntry[];
|
|
16
|
+
export declare function getRerenderCauses(profile: ParsedRenderProfile, limit?: number, minDuration?: number): RerenderCause[];
|
|
17
|
+
//# sourceMappingURL=analysis.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analysis.d.ts","sourceRoot":"","sources":["../../src/parser/analysis.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,gBAAgB,EAChB,mBAAmB,EAEnB,kBAAkB,EAClB,aAAa,EAId,MAAM,YAAY,CAAC;AA2DpB,wBAAgB,aAAa,CAC3B,OAAO,EAAE,mBAAmB,EAC5B,KAAK,SAAK,EACV,cAAc,SAAI,GACjB,gBAAgB,EAAE,CAuBpB;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,mBAAmB,EAAE,KAAK,SAAK,GAAG;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,aAAa,EAAE,kBAAkB,EAAE,CAAC;IACpC,UAAU,EAAE,gBAAgB,EAAE,CAAC;CAChC,CAaA;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,mBAAmB,EAC5B,KAAK,SAAK,EACV,MAAM,GAAE,OAAO,GAAG,SAAS,GAAG,KAAe,EAC7C,WAAW,SAAI,GACd,kBAAkB,EAAE,CA0BtB;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,mBAAmB,EAAE,KAAK,SAAK,EAAE,WAAW,SAAI,GAAG,aAAa,EAAE,CA6I5G"}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
function getScoreBand(score) {
|
|
2
|
+
if (score >= 6) {
|
|
3
|
+
return 'high';
|
|
4
|
+
}
|
|
5
|
+
if (score >= 3) {
|
|
6
|
+
return 'medium';
|
|
7
|
+
}
|
|
8
|
+
return 'low';
|
|
9
|
+
}
|
|
10
|
+
function getConfidence(evidenceCount) {
|
|
11
|
+
if (evidenceCount >= 3) {
|
|
12
|
+
return 'high';
|
|
13
|
+
}
|
|
14
|
+
if (evidenceCount >= 2) {
|
|
15
|
+
return 'medium';
|
|
16
|
+
}
|
|
17
|
+
return 'low';
|
|
18
|
+
}
|
|
19
|
+
function getCommitTopComponents(commit, limit) {
|
|
20
|
+
const components = new Map();
|
|
21
|
+
const totalActualDuration = commit.measurements.reduce((sum, measurement) => sum + measurement.actualDuration, 0);
|
|
22
|
+
for (const measurement of commit.measurements) {
|
|
23
|
+
const existing = components.get(measurement.componentName) ?? {
|
|
24
|
+
componentName: measurement.componentName,
|
|
25
|
+
actualDuration: 0,
|
|
26
|
+
selfDuration: 0,
|
|
27
|
+
renderCount: 0,
|
|
28
|
+
shareOfCommitWork: 0,
|
|
29
|
+
};
|
|
30
|
+
existing.actualDuration += measurement.actualDuration;
|
|
31
|
+
existing.selfDuration += measurement.selfDuration;
|
|
32
|
+
existing.renderCount += measurement.renderCount;
|
|
33
|
+
components.set(measurement.componentName, existing);
|
|
34
|
+
}
|
|
35
|
+
return Array.from(components.values())
|
|
36
|
+
.sort((left, right) => right.actualDuration - left.actualDuration)
|
|
37
|
+
.slice(0, limit)
|
|
38
|
+
.map(component => ({
|
|
39
|
+
...component,
|
|
40
|
+
shareOfCommitWork: totalActualDuration > 0
|
|
41
|
+
? component.actualDuration / totalActualDuration
|
|
42
|
+
: 0,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
export function getHotCommits(profile, limit = 10, componentLimit = 3) {
|
|
46
|
+
return profile.commits
|
|
47
|
+
.map(commit => ({
|
|
48
|
+
commitIndex: commit.index,
|
|
49
|
+
duration: commit.duration,
|
|
50
|
+
totalActualDuration: commit.measurements.reduce((sum, measurement) => sum + measurement.actualDuration, 0),
|
|
51
|
+
timestamp: commit.timestamp,
|
|
52
|
+
priorityLevel: commit.priorityLevel,
|
|
53
|
+
measurementCount: commit.measurements.length,
|
|
54
|
+
topComponents: getCommitTopComponents(commit, componentLimit),
|
|
55
|
+
updaterComponentNames: commit.updaterComponentNames,
|
|
56
|
+
}))
|
|
57
|
+
.sort((left, right) => {
|
|
58
|
+
if (right.duration !== left.duration) {
|
|
59
|
+
return right.duration - left.duration;
|
|
60
|
+
}
|
|
61
|
+
return right.totalActualDuration - left.totalActualDuration;
|
|
62
|
+
})
|
|
63
|
+
.slice(0, limit);
|
|
64
|
+
}
|
|
65
|
+
export function getRenderSummary(profile, limit = 10) {
|
|
66
|
+
return {
|
|
67
|
+
profileId: profile.id,
|
|
68
|
+
filename: profile.filename,
|
|
69
|
+
version: profile.version,
|
|
70
|
+
rendererId: profile.rendererId,
|
|
71
|
+
commitCount: profile.commits.length,
|
|
72
|
+
componentCount: profile.components.length,
|
|
73
|
+
totalCommitDuration: profile.totalCommitDuration,
|
|
74
|
+
totalRenderDuration: profile.totalRenderDuration,
|
|
75
|
+
hotCommits: getHotCommits(profile, Math.min(3, profile.commits.length), 3),
|
|
76
|
+
topComponents: getSlowComponents(profile, limit),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function getSlowComponents(profile, limit = 10, sortBy = 'total', minDuration = 0) {
|
|
80
|
+
const entries = profile.components
|
|
81
|
+
.filter(component => component.totalActualDuration >= minDuration)
|
|
82
|
+
.map(component => ({
|
|
83
|
+
componentName: component.componentName,
|
|
84
|
+
renderCount: component.renderCount,
|
|
85
|
+
mountCount: component.mountCount,
|
|
86
|
+
updateCount: component.updateCount,
|
|
87
|
+
nestedUpdateCount: component.nestedUpdateCount,
|
|
88
|
+
totalActualDuration: component.totalActualDuration,
|
|
89
|
+
averageActualDuration: component.renderCount > 0
|
|
90
|
+
? component.totalActualDuration / component.renderCount
|
|
91
|
+
: 0,
|
|
92
|
+
maxActualDuration: component.maxActualDuration,
|
|
93
|
+
commitCount: component.commitIndices.length,
|
|
94
|
+
}));
|
|
95
|
+
if (sortBy === 'average') {
|
|
96
|
+
entries.sort((a, b) => b.averageActualDuration - a.averageActualDuration);
|
|
97
|
+
}
|
|
98
|
+
else if (sortBy === 'max') {
|
|
99
|
+
entries.sort((a, b) => b.maxActualDuration - a.maxActualDuration);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
entries.sort((a, b) => b.totalActualDuration - a.totalActualDuration);
|
|
103
|
+
}
|
|
104
|
+
return entries.slice(0, limit);
|
|
105
|
+
}
|
|
106
|
+
export function getRerenderCauses(profile, limit = 10, minDuration = 0) {
|
|
107
|
+
const causes = profile.components
|
|
108
|
+
.filter(component => (component.updateCount > 0 || component.nestedUpdateCount > 0) &&
|
|
109
|
+
component.totalActualDuration >= minDuration)
|
|
110
|
+
.map(component => {
|
|
111
|
+
const likelyCauses = [];
|
|
112
|
+
const evidence = [];
|
|
113
|
+
const selfToActualRatio = component.totalActualDuration > 0
|
|
114
|
+
? component.totalSelfDuration / component.totalActualDuration
|
|
115
|
+
: 0;
|
|
116
|
+
if (component.updateCount >= 2) {
|
|
117
|
+
const updatePct = component.renderCount > 0
|
|
118
|
+
? Math.round((component.updateCount / component.renderCount) * 100)
|
|
119
|
+
: 0;
|
|
120
|
+
const mountPct = 100 - updatePct;
|
|
121
|
+
let causeText;
|
|
122
|
+
if (updatePct >= 80) {
|
|
123
|
+
causeText =
|
|
124
|
+
`${component.componentName} rendered in update phase ${component.updateCount} of ${component.renderCount} times (${updatePct}% updates, ${mountPct}% mounts). ` +
|
|
125
|
+
`It almost never remounts fresh, which means it is staying alive and re-rendering in place repeatedly. ` +
|
|
126
|
+
`The likely driver is a parent passing new object/array/function references on every render, or the component's own state being set more often than intended. ` +
|
|
127
|
+
`Check for inline object props, unstable callback references, or a context value that changes on every parent render.`;
|
|
128
|
+
}
|
|
129
|
+
else if (updatePct >= 40) {
|
|
130
|
+
causeText =
|
|
131
|
+
`${component.componentName} has a mixed mount/update pattern: ${updatePct}% updates, ${mountPct}% mounts across ${component.renderCount} renders. ` +
|
|
132
|
+
`The component is sometimes remounting fresh and sometimes updating in place, which can mean it is conditionally included in the tree, ` +
|
|
133
|
+
`or its React key changes on some renders (causing a full unmount+mount). ` +
|
|
134
|
+
`Investigate whether key changes are intentional and whether conditional rendering could be replaced with CSS visibility to avoid remount cost.`;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
causeText =
|
|
138
|
+
`${component.componentName} is mostly mounting rather than updating: ${mountPct}% mounts vs ${updatePct}% updates across ${component.renderCount} renders. ` +
|
|
139
|
+
`This level of remounting usually points to unstable list keys, a parent that conditionally unmounts and recreates the component, ` +
|
|
140
|
+
`or a component defined inside a render function (which creates a new type on every render and forces React to unmount instead of update). ` +
|
|
141
|
+
`Ensure keys are stable identifiers and that the component is not defined inline.`;
|
|
142
|
+
}
|
|
143
|
+
likelyCauses.push(causeText);
|
|
144
|
+
evidence.push({
|
|
145
|
+
signal: 'repeated-update-phases',
|
|
146
|
+
observed: component.updateCount,
|
|
147
|
+
threshold: 2,
|
|
148
|
+
detail: `${component.componentName} entered the update phase ${component.updateCount} times (${updatePct}%) across ${component.commitIndices.length} commit${component.commitIndices.length === 1 ? '' : 's'}. Mount/update ratio: ${mountPct}% mounts, ${updatePct}% updates.`,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (component.nestedUpdateCount > 0) {
|
|
152
|
+
likelyCauses.push(`${component.nestedUpdateCount} of ${component.componentName}'s renders were triggered as nested updates ` +
|
|
153
|
+
`(the component itself was listed as an updater for its own commit). ` +
|
|
154
|
+
`This is a strong indicator of a render-then-setState loop, an effect with missing or incorrect dependencies, ` +
|
|
155
|
+
`or a context provider that updates its value during render.`);
|
|
156
|
+
evidence.push({
|
|
157
|
+
signal: 'nested-update-propagation',
|
|
158
|
+
observed: component.nestedUpdateCount,
|
|
159
|
+
threshold: 1,
|
|
160
|
+
detail: `${component.componentName} appeared as an updater in ${component.nestedUpdateCount} of its own commits, which is a strong hint that rerender work is self-propagating inside this subtree.`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (selfToActualRatio >= 0.9) {
|
|
164
|
+
const selfPct = Math.round(selfToActualRatio * 100);
|
|
165
|
+
likelyCauses.push(`${selfPct}% of ${component.componentName}'s render cost is self-contained (not delegated to children). ` +
|
|
166
|
+
`The component body itself is doing the heavy lifting — likely expensive JSX construction, inline computations, or heavy formatting on each render. ` +
|
|
167
|
+
`Wrapping with React.memo will prevent unnecessary calls, and moving expensive calculations into useMemo will avoid recomputing them when unrelated state changes.`);
|
|
168
|
+
evidence.push({
|
|
169
|
+
signal: 'self-intensive-render',
|
|
170
|
+
observed: Number(selfToActualRatio.toFixed(2)),
|
|
171
|
+
threshold: 0.9,
|
|
172
|
+
detail: `Self duration is ${selfPct}% of actual duration for ${component.componentName}, indicating this component does most render work in its own body rather than delegating to children. Direct memoization of this component is the correct fix.`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (component.commitIndices.length >= 3) {
|
|
176
|
+
const commitList = component.commitIndices.slice(0, 5).join(', ') +
|
|
177
|
+
(component.commitIndices.length > 5 ? ` … (+${component.commitIndices.length - 5} more)` : '');
|
|
178
|
+
likelyCauses.push(`${component.componentName} appeared in ${component.commitIndices.length} separate commits (commits ${commitList}), ` +
|
|
179
|
+
`which means rerender pressure on this component is sustained across the session rather than a one-off spike. ` +
|
|
180
|
+
`Likely causes are an unstable context value, a subscription or timer that triggers frequent state updates, ` +
|
|
181
|
+
`or a prop that carries a new reference on every parent render.`);
|
|
182
|
+
evidence.push({
|
|
183
|
+
signal: 'wide-commit-spread',
|
|
184
|
+
observed: component.commitIndices.length,
|
|
185
|
+
threshold: 3,
|
|
186
|
+
detail: `${component.componentName} shows up across ${component.commitIndices.length} commits, which usually means rerender pressure is sustained rather than a one-off spike.`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (likelyCauses.length === 0) {
|
|
190
|
+
likelyCauses.push(`${component.componentName} rendered ${component.updateCount} time${component.updateCount === 1 ? '' : 's'} in update phase, ` +
|
|
191
|
+
`but the signals are below the thresholds for specific heuristics. ` +
|
|
192
|
+
`React Profiler exports do not include prop/state diffs, so the exact trigger cannot be determined from this data alone. ` +
|
|
193
|
+
`Use why-did-you-render or React DevTools to capture the specific prop or state change.`);
|
|
194
|
+
evidence.push({
|
|
195
|
+
signal: 'limited-export-evidence',
|
|
196
|
+
observed: component.commitIndices.length,
|
|
197
|
+
threshold: 'n/a',
|
|
198
|
+
detail: `React Profiler exports do not include exact prop/state diffs, so only weak rerender signals were available for ${component.componentName}.`,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
const score = Math.min(10, Number((Math.min(3, component.updateCount)
|
|
202
|
+
+ Math.min(3, component.nestedUpdateCount * 1.5)
|
|
203
|
+
+ (component.commitIndices.length >= 3 ? 2 : component.commitIndices.length >= 2 ? 1 : 0)
|
|
204
|
+
+ (selfToActualRatio >= 0.9 ? 2 : selfToActualRatio >= 0.75 ? 1 : 0)).toFixed(1)));
|
|
205
|
+
return {
|
|
206
|
+
componentName: component.componentName,
|
|
207
|
+
renderCount: component.renderCount,
|
|
208
|
+
updateCount: component.updateCount,
|
|
209
|
+
nestedUpdateCount: component.nestedUpdateCount,
|
|
210
|
+
totalActualDuration: component.totalActualDuration,
|
|
211
|
+
score,
|
|
212
|
+
scoreBand: getScoreBand(score),
|
|
213
|
+
confidence: getConfidence(evidence.filter(item => item.signal !== 'limited-export-evidence').length),
|
|
214
|
+
evidence,
|
|
215
|
+
likelyCauses,
|
|
216
|
+
};
|
|
217
|
+
})
|
|
218
|
+
.sort((left, right) => {
|
|
219
|
+
if (right.score !== left.score) {
|
|
220
|
+
return right.score - left.score;
|
|
221
|
+
}
|
|
222
|
+
return right.totalActualDuration - left.totalActualDuration;
|
|
223
|
+
});
|
|
224
|
+
return causes.slice(0, limit);
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=analysis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analysis.js","sourceRoot":"","sources":["../../src/parser/analysis.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react-profile.d.ts","sourceRoot":"","sources":["../../src/parser/react-profile.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAEV,mBAAmB,EAGpB,MAAM,YAAY,CAAC;AAuOpB,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAmEzF"}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Helpers
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
function asNumber(value, fallback = 0) {
|
|
7
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Build a fiberID -> displayName map from the snapshots array.
|
|
11
|
+
* snapshots is serialised as Array<[fiberID, SnapshotNode]> in the export.
|
|
12
|
+
*/
|
|
13
|
+
function buildSnapshotMap(snapshots) {
|
|
14
|
+
const map = new Map();
|
|
15
|
+
if (!Array.isArray(snapshots))
|
|
16
|
+
return map;
|
|
17
|
+
for (const entry of snapshots) {
|
|
18
|
+
if (!Array.isArray(entry) || entry.length < 2)
|
|
19
|
+
continue;
|
|
20
|
+
const [fiberID, node] = entry;
|
|
21
|
+
if (typeof fiberID !== 'number')
|
|
22
|
+
continue;
|
|
23
|
+
const raw = node;
|
|
24
|
+
const name = typeof raw?.displayName === 'string' && raw.displayName.trim().length > 0
|
|
25
|
+
? raw.displayName
|
|
26
|
+
: `(fiber:${fiberID})`;
|
|
27
|
+
map.set(fiberID, name);
|
|
28
|
+
}
|
|
29
|
+
return map;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build a Set of fiberIDs that existed before profiling started.
|
|
33
|
+
* initialTreeBaseDurations is serialised as Array<[fiberID, duration]>.
|
|
34
|
+
*/
|
|
35
|
+
function buildInitialFiberIDSet(initialTreeBaseDurations) {
|
|
36
|
+
const set = new Set();
|
|
37
|
+
if (!Array.isArray(initialTreeBaseDurations))
|
|
38
|
+
return set;
|
|
39
|
+
for (const entry of initialTreeBaseDurations) {
|
|
40
|
+
if (Array.isArray(entry) && typeof entry[0] === 'number') {
|
|
41
|
+
set.add(entry[0]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return set;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Parse a single commit from the React DevTools v5 export.
|
|
48
|
+
* fiberActualDurations and fiberSelfDurations are Array<[fiberID, duration]> tuples.
|
|
49
|
+
*/
|
|
50
|
+
function parseCommit(raw, index, snapshotMap, initialFiberIDs, seenFiberIDs) {
|
|
51
|
+
const fiberActualDurations = Array.isArray(raw.fiberActualDurations)
|
|
52
|
+
? raw.fiberActualDurations
|
|
53
|
+
: [];
|
|
54
|
+
// Build a quick self-duration lookup for this commit
|
|
55
|
+
const selfDurationMap = new Map();
|
|
56
|
+
if (Array.isArray(raw.fiberSelfDurations)) {
|
|
57
|
+
for (const entry of raw.fiberSelfDurations) {
|
|
58
|
+
if (Array.isArray(entry) &&
|
|
59
|
+
typeof entry[0] === 'number' &&
|
|
60
|
+
typeof entry[1] === 'number') {
|
|
61
|
+
selfDurationMap.set(entry[0], entry[1]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Build the set of fiber IDs that triggered this commit (React DevTools "updaters" field).
|
|
66
|
+
// updaters are the components that called setState / dispatched / caused this commit.
|
|
67
|
+
const updaterFiberIDs = new Set();
|
|
68
|
+
if (Array.isArray(raw.updaters)) {
|
|
69
|
+
for (const updater of raw.updaters) {
|
|
70
|
+
if (updater !== null && typeof updater === 'object') {
|
|
71
|
+
const u = updater;
|
|
72
|
+
if (typeof u['id'] === 'number') {
|
|
73
|
+
updaterFiberIDs.add(u['id']);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const seenUpdaterNames = new Set();
|
|
79
|
+
const updaterComponentNames = [];
|
|
80
|
+
for (const fiberID of updaterFiberIDs) {
|
|
81
|
+
const name = snapshotMap.get(fiberID) ?? `(fiber:${fiberID})`;
|
|
82
|
+
if (!seenUpdaterNames.has(name)) {
|
|
83
|
+
seenUpdaterNames.add(name);
|
|
84
|
+
updaterComponentNames.push(name);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const measurements = [];
|
|
88
|
+
for (const entry of fiberActualDurations) {
|
|
89
|
+
if (!Array.isArray(entry) || entry.length < 2)
|
|
90
|
+
continue;
|
|
91
|
+
const [fiberID, actualDuration] = entry;
|
|
92
|
+
if (typeof fiberID !== 'number' || typeof actualDuration !== 'number')
|
|
93
|
+
continue;
|
|
94
|
+
const componentName = snapshotMap.get(fiberID) ?? `(fiber:${fiberID})`;
|
|
95
|
+
const selfDuration = selfDurationMap.get(fiberID) ?? actualDuration;
|
|
96
|
+
// A fiber is mounting if it was not present when profiling started AND
|
|
97
|
+
// this is the first commit we have seen it in.
|
|
98
|
+
const isMount = !initialFiberIDs.has(fiberID) && !seenFiberIDs.has(fiberID);
|
|
99
|
+
seenFiberIDs.add(fiberID);
|
|
100
|
+
measurements.push({
|
|
101
|
+
componentName,
|
|
102
|
+
phase: isMount ? 'mount' : 'update',
|
|
103
|
+
actualDuration,
|
|
104
|
+
selfDuration,
|
|
105
|
+
startTime: asNumber(raw.timestamp),
|
|
106
|
+
commitTime: asNumber(raw.timestamp),
|
|
107
|
+
renderCount: 1,
|
|
108
|
+
commitIndex: index,
|
|
109
|
+
isNestedUpdate: !isMount && updaterFiberIDs.has(fiberID),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
index,
|
|
114
|
+
duration: asNumber(raw.duration),
|
|
115
|
+
timestamp: asNumber(raw.timestamp),
|
|
116
|
+
priorityLevel: typeof raw.priorityLevel === 'string' ? raw.priorityLevel : null,
|
|
117
|
+
measurements,
|
|
118
|
+
updaterComponentNames,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function buildComponentStats(commits) {
|
|
122
|
+
const stats = new Map();
|
|
123
|
+
const commitIndexSets = new Map();
|
|
124
|
+
for (const commit of commits) {
|
|
125
|
+
for (const measurement of commit.measurements) {
|
|
126
|
+
const name = measurement.componentName;
|
|
127
|
+
if (!stats.has(name)) {
|
|
128
|
+
stats.set(name, {
|
|
129
|
+
componentName: name,
|
|
130
|
+
renderCount: 0,
|
|
131
|
+
mountCount: 0,
|
|
132
|
+
updateCount: 0,
|
|
133
|
+
nestedUpdateCount: 0,
|
|
134
|
+
totalActualDuration: 0,
|
|
135
|
+
totalSelfDuration: 0,
|
|
136
|
+
maxActualDuration: 0,
|
|
137
|
+
commitIndices: [],
|
|
138
|
+
});
|
|
139
|
+
commitIndexSets.set(name, new Set());
|
|
140
|
+
}
|
|
141
|
+
const existing = stats.get(name);
|
|
142
|
+
existing.renderCount += 1;
|
|
143
|
+
existing.totalActualDuration += measurement.actualDuration;
|
|
144
|
+
existing.totalSelfDuration += measurement.selfDuration;
|
|
145
|
+
existing.maxActualDuration = Math.max(existing.maxActualDuration, measurement.actualDuration);
|
|
146
|
+
commitIndexSets.get(name).add(commit.index);
|
|
147
|
+
if (measurement.phase === 'mount') {
|
|
148
|
+
existing.mountCount += 1;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
existing.updateCount += 1;
|
|
152
|
+
if (measurement.isNestedUpdate) {
|
|
153
|
+
existing.nestedUpdateCount += 1;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (const [name, indexSet] of commitIndexSets) {
|
|
159
|
+
stats.get(name).commitIndices = Array.from(indexSet).sort((a, b) => a - b);
|
|
160
|
+
}
|
|
161
|
+
return Array.from(stats.values()).sort((a, b) => b.totalActualDuration - a.totalActualDuration);
|
|
162
|
+
}
|
|
163
|
+
export function parseRenderProfile(content, filename) {
|
|
164
|
+
let parsed;
|
|
165
|
+
try {
|
|
166
|
+
parsed = JSON.parse(content);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
throw new Error('Invalid render profile JSON');
|
|
170
|
+
}
|
|
171
|
+
// React DevTools has exported version 5 since early 2020 (PROFILER_EXPORT_VERSION = 5).
|
|
172
|
+
if (parsed.version !== 5) {
|
|
173
|
+
throw new Error(`Invalid render profile format: unsupported version "${String(parsed.version)}". ` +
|
|
174
|
+
'Expected a React DevTools Profiler export (version 5). ' +
|
|
175
|
+
'Open React DevTools -> Profiler tab -> record -> click the save icon.');
|
|
176
|
+
}
|
|
177
|
+
if (!Array.isArray(parsed.dataForRoots) || parsed.dataForRoots.length === 0) {
|
|
178
|
+
throw new Error('Invalid render profile format: no root data found. ' +
|
|
179
|
+
'Ensure you exported from the React DevTools Profiler tab, not the browser Performance tab.');
|
|
180
|
+
}
|
|
181
|
+
const allCommits = [];
|
|
182
|
+
for (const rawRoot of parsed.dataForRoots) {
|
|
183
|
+
const root = rawRoot;
|
|
184
|
+
// fiberID -> displayName, built from the serialised snapshots map
|
|
185
|
+
const snapshotMap = buildSnapshotMap(root.snapshots);
|
|
186
|
+
// Fibers present before profiling started (used for mount vs update detection)
|
|
187
|
+
const initialFiberIDs = buildInitialFiberIDSet(root.initialTreeBaseDurations);
|
|
188
|
+
const seenFiberIDs = new Set();
|
|
189
|
+
const commitDataArray = Array.isArray(root.commitData)
|
|
190
|
+
? root.commitData
|
|
191
|
+
: [];
|
|
192
|
+
const startIndex = allCommits.length;
|
|
193
|
+
for (let i = 0; i < commitDataArray.length; i++) {
|
|
194
|
+
allCommits.push(parseCommit(commitDataArray[i], startIndex + i, snapshotMap, initialFiberIDs, seenFiberIDs));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (allCommits.length === 0) {
|
|
198
|
+
throw new Error('No profiling commits found. The profiler may have been started but no renders occurred.');
|
|
199
|
+
}
|
|
200
|
+
const components = buildComponentStats(allCommits);
|
|
201
|
+
return {
|
|
202
|
+
id: randomUUID(),
|
|
203
|
+
filename: basename(filename),
|
|
204
|
+
version: '5',
|
|
205
|
+
rendererId: null,
|
|
206
|
+
commits: allCommits,
|
|
207
|
+
components,
|
|
208
|
+
totalCommitDuration: allCommits.reduce((sum, c) => sum + c.duration, 0),
|
|
209
|
+
totalRenderDuration: components.reduce((sum, c) => sum + c.totalActualDuration, 0),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=react-profile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react-profile.js","sourceRoot":"","sources":["../../src/parser/react-profile.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export interface RenderMeasurement {
|
|
2
|
+
componentName: string;
|
|
3
|
+
phase: string;
|
|
4
|
+
actualDuration: number;
|
|
5
|
+
selfDuration: number;
|
|
6
|
+
startTime: number;
|
|
7
|
+
commitTime: number;
|
|
8
|
+
renderCount: number;
|
|
9
|
+
commitIndex: number;
|
|
10
|
+
isNestedUpdate: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface RenderCommit {
|
|
13
|
+
index: number;
|
|
14
|
+
duration: number;
|
|
15
|
+
timestamp: number;
|
|
16
|
+
priorityLevel: string | null;
|
|
17
|
+
measurements: RenderMeasurement[];
|
|
18
|
+
updaterComponentNames: string[];
|
|
19
|
+
}
|
|
20
|
+
export interface ComponentStats {
|
|
21
|
+
componentName: string;
|
|
22
|
+
renderCount: number;
|
|
23
|
+
mountCount: number;
|
|
24
|
+
updateCount: number;
|
|
25
|
+
nestedUpdateCount: number;
|
|
26
|
+
totalActualDuration: number;
|
|
27
|
+
totalSelfDuration: number;
|
|
28
|
+
maxActualDuration: number;
|
|
29
|
+
commitIndices: number[];
|
|
30
|
+
}
|
|
31
|
+
export interface ParsedRenderProfile {
|
|
32
|
+
id: string;
|
|
33
|
+
filename: string;
|
|
34
|
+
version: string;
|
|
35
|
+
rendererId: number | null;
|
|
36
|
+
commits: RenderCommit[];
|
|
37
|
+
components: ComponentStats[];
|
|
38
|
+
totalCommitDuration: number;
|
|
39
|
+
totalRenderDuration: number;
|
|
40
|
+
}
|
|
41
|
+
export interface RenderSummaryEntry {
|
|
42
|
+
componentName: string;
|
|
43
|
+
renderCount: number;
|
|
44
|
+
mountCount: number;
|
|
45
|
+
updateCount: number;
|
|
46
|
+
nestedUpdateCount: number;
|
|
47
|
+
totalActualDuration: number;
|
|
48
|
+
averageActualDuration: number;
|
|
49
|
+
maxActualDuration: number;
|
|
50
|
+
commitCount: number;
|
|
51
|
+
}
|
|
52
|
+
export interface HotCommitComponentSummary {
|
|
53
|
+
componentName: string;
|
|
54
|
+
actualDuration: number;
|
|
55
|
+
selfDuration: number;
|
|
56
|
+
renderCount: number;
|
|
57
|
+
shareOfCommitWork: number;
|
|
58
|
+
}
|
|
59
|
+
export interface HotCommitSummary {
|
|
60
|
+
commitIndex: number;
|
|
61
|
+
duration: number;
|
|
62
|
+
totalActualDuration: number;
|
|
63
|
+
timestamp: number;
|
|
64
|
+
priorityLevel: string | null;
|
|
65
|
+
measurementCount: number;
|
|
66
|
+
topComponents: HotCommitComponentSummary[];
|
|
67
|
+
updaterComponentNames: string[];
|
|
68
|
+
}
|
|
69
|
+
export interface RerenderEvidence {
|
|
70
|
+
signal: string;
|
|
71
|
+
observed: number | string;
|
|
72
|
+
threshold: number | string;
|
|
73
|
+
detail: string;
|
|
74
|
+
}
|
|
75
|
+
export type RerenderConfidence = 'low' | 'medium' | 'high';
|
|
76
|
+
export type RerenderScoreBand = 'low' | 'medium' | 'high';
|
|
77
|
+
export interface RerenderCause {
|
|
78
|
+
componentName: string;
|
|
79
|
+
renderCount: number;
|
|
80
|
+
updateCount: number;
|
|
81
|
+
nestedUpdateCount: number;
|
|
82
|
+
totalActualDuration: number;
|
|
83
|
+
score: number;
|
|
84
|
+
scoreBand: RerenderScoreBand;
|
|
85
|
+
confidence: RerenderConfidence;
|
|
86
|
+
evidence: RerenderEvidence[];
|
|
87
|
+
likelyCauses: string[];
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/parser/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/parser/types.ts"],"names":[],"mappings":""}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ParsedRenderProfile } from './parser/types.js';
|
|
2
|
+
export declare function storeRenderProfile(profile: ParsedRenderProfile): void;
|
|
3
|
+
export declare function getRenderProfile(id: string): ParsedRenderProfile | undefined;
|
|
4
|
+
export declare function listRenderProfiles(): Array<{
|
|
5
|
+
id: string;
|
|
6
|
+
filename: string;
|
|
7
|
+
commitCount: number;
|
|
8
|
+
componentCount: number;
|
|
9
|
+
totalCommitDuration: number;
|
|
10
|
+
}>;
|
|
11
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAI7D,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAErE;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAE5E;AAED,wBAAgB,kBAAkB,IAAI,KAAK,CAAC;IAC1C,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,mBAAmB,EAAE,MAAM,CAAC;CAC7B,CAAC,CAQD"}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const profiles = new Map();
|
|
2
|
+
export function storeRenderProfile(profile) {
|
|
3
|
+
profiles.set(profile.id, profile);
|
|
4
|
+
}
|
|
5
|
+
export function getRenderProfile(id) {
|
|
6
|
+
return profiles.get(id);
|
|
7
|
+
}
|
|
8
|
+
export function listRenderProfiles() {
|
|
9
|
+
return Array.from(profiles.values()).map(profile => ({
|
|
10
|
+
id: profile.id,
|
|
11
|
+
filename: profile.filename,
|
|
12
|
+
commitCount: profile.commits.length,
|
|
13
|
+
componentCount: profile.components.length,
|
|
14
|
+
totalCommitDuration: profile.totalCommitDuration,
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAEA,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA+B,CAAC;AAExD,MAAM,UAAU,kBAAkB,CAAC,OAA4B;IAC7D,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,EAAU;IACzC,OAAO,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,kBAAkB;IAOhC,OAAO,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACnD,EAAE,EAAE,OAAO,CAAC,EAAE;QACd,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,WAAW,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM;QACnC,cAAc,EAAE,OAAO,CAAC,UAAU,CAAC,MAAM;QACzC,mBAAmB,EAAE,OAAO,CAAC,mBAAmB;KACjD,CAAC,CAAC,CAAC;AACN,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-hot-commits.d.ts","sourceRoot":"","sources":["../../src/tools/get-hot-commits.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAOpE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAsC7D"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { formatMs, formatPct } from '../format.js';
|
|
3
|
+
import { getHotCommits } from '../parser/analysis.js';
|
|
4
|
+
import { getRenderProfile } from '../store.js';
|
|
5
|
+
export function registerGetHotCommits(server) {
|
|
6
|
+
server.registerTool('get_hot_commits', {
|
|
7
|
+
title: 'Get Hot Commits',
|
|
8
|
+
description: 'Rank the most expensive commits in a loaded render profile and show the top components inside each spike.',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
profileId: z.string().describe('Profile ID from load_render_profile'),
|
|
11
|
+
limit: z.number().int().positive().max(25).optional().describe('How many commits to include. Defaults to 10.'),
|
|
12
|
+
componentLimit: z.number().int().positive().max(10).optional().describe('How many top components to include per commit. Defaults to 3.'),
|
|
13
|
+
},
|
|
14
|
+
}, async ({ profileId, limit, componentLimit }) => {
|
|
15
|
+
const profile = getRenderProfile(profileId);
|
|
16
|
+
if (!profile) {
|
|
17
|
+
throw new Error(`Profile "${profileId}" not found. Call get_render_summary without a profileId to list all loaded profiles.`);
|
|
18
|
+
}
|
|
19
|
+
const hotCommits = getHotCommits(profile, limit ?? 10, componentLimit ?? 3).map(commit => ({
|
|
20
|
+
...commit,
|
|
21
|
+
duration: formatMs(commit.duration),
|
|
22
|
+
totalActualDuration: formatMs(commit.totalActualDuration),
|
|
23
|
+
topComponents: commit.topComponents.map(component => ({
|
|
24
|
+
...component,
|
|
25
|
+
actualDuration: formatMs(component.actualDuration),
|
|
26
|
+
selfDuration: formatMs(component.selfDuration),
|
|
27
|
+
shareOfCommitWork: formatPct(component.shareOfCommitWork),
|
|
28
|
+
})),
|
|
29
|
+
}));
|
|
30
|
+
return {
|
|
31
|
+
content: [{
|
|
32
|
+
type: 'text',
|
|
33
|
+
text: JSON.stringify({
|
|
34
|
+
profileId,
|
|
35
|
+
timestampNote: 'timestamp values are milliseconds since the profiling session started, not Unix epoch',
|
|
36
|
+
hotCommits,
|
|
37
|
+
}, null, 2),
|
|
38
|
+
}],
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=get-hot-commits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-hot-commits.js","sourceRoot":"","sources":["../../src/tools/get-hot-commits.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,MAAM,UAAU,qBAAqB,CAAC,MAAiB;IACrD,MAAM,CAAC,YAAY,CAAC,iBAAiB,EAAE;QACrC,KAAK,EAAE,iBAAiB;QACxB,WAAW,EAAE,2GAA2G;QACxH,WAAW,EAAE;YACX,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,qCAAqC,CAAC;YACrE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,8CAA8C,CAAC;YAC9G,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,+DAA+D,CAAC;SACzI;KACF,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,EAAE;QAChD,MAAM,OAAO,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;QAC5C,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CAAC,YAAY,SAAS,uFAAuF,CAAC,CAAC;QAChI,CAAC;QAED,MAAM,UAAU,GAAG,aAAa,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE,EAAE,cAAc,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACzF,GAAG,MAAM;YACT,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC;YACnC,mBAAmB,EAAE,QAAQ,CAAC,MAAM,CAAC,mBAAmB,CAAC;YACzD,aAAa,EAAE,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;gBACpD,GAAG,SAAS;gBACZ,cAAc,EAAE,QAAQ,CAAC,SAAS,CAAC,cAAc,CAAC;gBAClD,YAAY,EAAE,QAAQ,CAAC,SAAS,CAAC,YAAY,CAAC;gBAC9C,iBAAiB,EAAE,SAAS,CAAC,SAAS,CAAC,iBAAiB,CAAC;aAC1D,CAAC,CAAC;SACJ,CAAC,CAAC,CAAC;QAEJ,OAAO;YACL,OAAO,EAAE,CAAC;oBACR,IAAI,EAAE,MAAe;oBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;wBACnB,SAAS;wBACT,aAAa,EAAE,uFAAuF;wBACtG,UAAU;qBACX,EAAE,IAAI,EAAE,CAAC,CAAC;iBACZ,CAAC;SACH,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-render-summary.d.ts","sourceRoot":"","sources":["../../src/tools/get-render-summary.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAOpE,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA0DhE"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { formatMs, formatPct } from '../format.js';
|
|
3
|
+
import { getRenderSummary as summarizeRenderProfile } from '../parser/analysis.js';
|
|
4
|
+
import { getRenderProfile, listRenderProfiles } from '../store.js';
|
|
5
|
+
export function registerGetRenderSummary(server) {
|
|
6
|
+
server.registerTool('get_render_summary', {
|
|
7
|
+
title: 'Get Render Summary',
|
|
8
|
+
description: 'Summarize the loaded Next.js render profile, including top components by render cost.',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
profileId: z.string().optional().describe('Profile ID from load_render_profile. Omit to list loaded profiles.'),
|
|
11
|
+
limit: z.number().int().positive().max(25).optional().describe('How many top components to include. Defaults to 10.'),
|
|
12
|
+
},
|
|
13
|
+
}, async ({ profileId, limit }) => {
|
|
14
|
+
if (!profileId) {
|
|
15
|
+
return {
|
|
16
|
+
content: [{
|
|
17
|
+
type: 'text',
|
|
18
|
+
text: JSON.stringify({
|
|
19
|
+
profiles: listRenderProfiles().map(profile => ({
|
|
20
|
+
...profile,
|
|
21
|
+
totalCommitDuration: `${profile.totalCommitDuration.toFixed(2)}ms`,
|
|
22
|
+
})),
|
|
23
|
+
}, null, 2),
|
|
24
|
+
}],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const profile = getRenderProfile(profileId);
|
|
28
|
+
if (!profile) {
|
|
29
|
+
throw new Error(`Profile "${profileId}" not found. Call get_render_summary without a profileId to list all loaded profiles.`);
|
|
30
|
+
}
|
|
31
|
+
const summary = summarizeRenderProfile(profile, limit ?? 10);
|
|
32
|
+
return {
|
|
33
|
+
content: [{
|
|
34
|
+
type: 'text',
|
|
35
|
+
text: JSON.stringify({
|
|
36
|
+
...summary,
|
|
37
|
+
totalCommitDuration: formatMs(summary.totalCommitDuration),
|
|
38
|
+
totalRenderDuration: formatMs(summary.totalRenderDuration),
|
|
39
|
+
topComponents: summary.topComponents.map(component => ({
|
|
40
|
+
...component,
|
|
41
|
+
totalActualDuration: formatMs(component.totalActualDuration),
|
|
42
|
+
averageActualDuration: formatMs(component.averageActualDuration),
|
|
43
|
+
maxActualDuration: formatMs(component.maxActualDuration),
|
|
44
|
+
})),
|
|
45
|
+
hotCommits: summary.hotCommits.map(commit => ({
|
|
46
|
+
...commit,
|
|
47
|
+
duration: formatMs(commit.duration),
|
|
48
|
+
totalActualDuration: formatMs(commit.totalActualDuration),
|
|
49
|
+
topComponents: commit.topComponents.map(component => ({
|
|
50
|
+
...component,
|
|
51
|
+
actualDuration: formatMs(component.actualDuration),
|
|
52
|
+
selfDuration: formatMs(component.selfDuration),
|
|
53
|
+
shareOfCommitWork: formatPct(component.shareOfCommitWork),
|
|
54
|
+
})),
|
|
55
|
+
})),
|
|
56
|
+
}, null, 2),
|
|
57
|
+
}],
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=get-render-summary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-render-summary.js","sourceRoot":"","sources":["../../src/tools/get-render-summary.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-rerender-causes.d.ts","sourceRoot":"","sources":["../../src/tools/get-rerender-causes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAOpE,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA8CjE"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { formatMs } from '../format.js';
|
|
3
|
+
import { getRerenderCauses } from '../parser/analysis.js';
|
|
4
|
+
import { getRenderProfile } from '../store.js';
|
|
5
|
+
export function registerGetRerenderCauses(server) {
|
|
6
|
+
server.registerTool('get_rerender_causes', {
|
|
7
|
+
title: 'Get Rerender Causes',
|
|
8
|
+
description: 'Highlight components with repeated rerenders and explain likely causes using heuristics derived from the exported profile.',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
profileId: z.string().describe('Profile ID from load_render_profile'),
|
|
11
|
+
limit: z.number().int().positive().max(25).optional().describe('How many components to include. Defaults to 10.'),
|
|
12
|
+
minDuration: z.number().nonnegative().optional().describe('Minimum total actual duration in ms for a component to appear. Filters sub-millisecond noise. Defaults to 0.'),
|
|
13
|
+
},
|
|
14
|
+
}, async ({ profileId, limit, minDuration }) => {
|
|
15
|
+
const profile = getRenderProfile(profileId);
|
|
16
|
+
if (!profile) {
|
|
17
|
+
throw new Error(`Profile "${profileId}" not found. Call get_render_summary without a profileId to list all loaded profiles.`);
|
|
18
|
+
}
|
|
19
|
+
const causes = getRerenderCauses(profile, limit ?? 10, minDuration ?? 0).map(cause => ({
|
|
20
|
+
...cause,
|
|
21
|
+
score: Number(cause.score.toFixed(1)),
|
|
22
|
+
totalActualDuration: formatMs(cause.totalActualDuration),
|
|
23
|
+
}));
|
|
24
|
+
return {
|
|
25
|
+
content: [{
|
|
26
|
+
type: 'text',
|
|
27
|
+
text: JSON.stringify({
|
|
28
|
+
profileId,
|
|
29
|
+
note: 'Rerender causes are heuristic because exported React Profiler JSON does not include direct prop/state diff metadata.',
|
|
30
|
+
scoreDocumentation: {
|
|
31
|
+
meaning: '0-10 heuristic rerender risk score. Higher means stronger evidence that repeated rerenders are worth investigating.',
|
|
32
|
+
thresholds: {
|
|
33
|
+
low: '0.0-2.9',
|
|
34
|
+
medium: '3.0-5.9',
|
|
35
|
+
high: '6.0-10.0',
|
|
36
|
+
},
|
|
37
|
+
factors: [
|
|
38
|
+
'Repeated update-phase renders',
|
|
39
|
+
'Nested update propagation (component listed as updater for its own commit)',
|
|
40
|
+
'Presence across many commits',
|
|
41
|
+
'Self-intensive render (high self-to-actual ratio, low child delegation)',
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
causes,
|
|
45
|
+
}, null, 2),
|
|
46
|
+
}],
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=get-rerender-causes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-rerender-causes.js","sourceRoot":"","sources":["../../src/tools/get-rerender-causes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,MAAM,UAAU,yBAAyB,CAAC,MAAiB;IACzD,MAAM,CAAC,YAAY,CAAC,qBAAqB,EAAE;QACzC,KAAK,EAAE,qBAAqB;QAC5B,WAAW,EAAE,4HAA4H;QACzI,WAAW,EAAE;YACX,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,qCAAqC,CAAC;YACrE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,iDAAiD,CAAC;YACjH,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,8GAA8G,CAAC;SAC1K;KACF,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE;QAC7C,MAAM,OAAO,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;QAC5C,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CAAC,YAAY,SAAS,uFAAuF,CAAC,CAAC;QAChI,CAAC;QAED,MAAM,MAAM,GAAG,iBAAiB,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE,EAAE,WAAW,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACrF,GAAG,KAAK;YACR,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YACrC,mBAAmB,EAAE,QAAQ,CAAC,KAAK,CAAC,mBAAmB,CAAC;SACzD,CAAC,CAAC,CAAC;QAEJ,OAAO;YACL,OAAO,EAAE,CAAC;oBACR,IAAI,EAAE,MAAe;oBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;wBACnB,SAAS;wBACT,IAAI,EAAE,sHAAsH;wBAC5H,kBAAkB,EAAE;4BAClB,OAAO,EAAE,qHAAqH;4BAC9H,UAAU,EAAE;gCACV,GAAG,EAAE,SAAS;gCACd,MAAM,EAAE,SAAS;gCACjB,IAAI,EAAE,UAAU;6BACjB;4BACD,OAAO,EAAE;gCACP,+BAA+B;gCAC/B,4EAA4E;gCAC5E,8BAA8B;gCAC9B,yEAAyE;6BAC1E;yBACF;wBACD,MAAM;qBACP,EAAE,IAAI,EAAE,CAAC,CAAC;iBACZ,CAAC;SACH,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-slow-components.d.ts","sourceRoot":"","sources":["../../src/tools/get-slow-components.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAOpE,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA+BjE"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { formatMs } from '../format.js';
|
|
3
|
+
import { getSlowComponents } from '../parser/analysis.js';
|
|
4
|
+
import { getRenderProfile } from '../store.js';
|
|
5
|
+
export function registerGetSlowComponents(server) {
|
|
6
|
+
server.registerTool('get_slow_components', {
|
|
7
|
+
title: 'Get Slow Components',
|
|
8
|
+
description: 'Return the slowest components in a loaded Next.js render profile, ranked by total actual render time.',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
profileId: z.string().describe('Profile ID from load_render_profile'),
|
|
11
|
+
limit: z.number().int().positive().max(25).optional().describe('How many components to return. Defaults to 10.'),
|
|
12
|
+
sortBy: z.enum(['total', 'average', 'max']).optional().describe('Sort metric: "total" = total render time (default), "average" = average per render, "max" = peak single render time.'),
|
|
13
|
+
minDuration: z.number().nonnegative().optional().describe('Minimum total actual duration in ms to include. Filters sub-millisecond noise. Defaults to 0.'),
|
|
14
|
+
},
|
|
15
|
+
}, async ({ profileId, limit, sortBy, minDuration }) => {
|
|
16
|
+
const profile = getRenderProfile(profileId);
|
|
17
|
+
if (!profile) {
|
|
18
|
+
throw new Error(`Profile "${profileId}" not found. Call get_render_summary without a profileId to list all loaded profiles.`);
|
|
19
|
+
}
|
|
20
|
+
const slowComponents = getSlowComponents(profile, limit ?? 10, sortBy ?? 'total', minDuration ?? 0).map((component, index) => ({
|
|
21
|
+
rank: index + 1,
|
|
22
|
+
...component,
|
|
23
|
+
totalActualDuration: formatMs(component.totalActualDuration),
|
|
24
|
+
averageActualDuration: formatMs(component.averageActualDuration),
|
|
25
|
+
maxActualDuration: formatMs(component.maxActualDuration),
|
|
26
|
+
}));
|
|
27
|
+
return {
|
|
28
|
+
content: [{
|
|
29
|
+
type: 'text',
|
|
30
|
+
text: JSON.stringify({ profileId, slowComponents }, null, 2),
|
|
31
|
+
}],
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=get-slow-components.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-slow-components.js","sourceRoot":"","sources":["../../src/tools/get-slow-components.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,MAAM,UAAU,yBAAyB,CAAC,MAAiB;IACzD,MAAM,CAAC,YAAY,CAAC,qBAAqB,EAAE;QACzC,KAAK,EAAE,qBAAqB;QAC5B,WAAW,EAAE,uGAAuG;QACpH,WAAW,EAAE;YACX,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,qCAAqC,CAAC;YACrE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gDAAgD,CAAC;YAChH,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,sHAAsH,CAAC;YACvL,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,+FAA+F,CAAC;SAC3J;KACF,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,EAAE;QACrD,MAAM,OAAO,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;QAC5C,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CAAC,YAAY,SAAS,uFAAuF,CAAC,CAAC;QAChI,CAAC;QAED,MAAM,cAAc,GAAG,iBAAiB,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE,EAAE,MAAM,IAAI,OAAO,EAAE,WAAW,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;YAC7H,IAAI,EAAE,KAAK,GAAG,CAAC;YACf,GAAG,SAAS;YACZ,mBAAmB,EAAE,QAAQ,CAAC,SAAS,CAAC,mBAAmB,CAAC;YAC5D,qBAAqB,EAAE,QAAQ,CAAC,SAAS,CAAC,qBAAqB,CAAC;YAChE,iBAAiB,EAAE,QAAQ,CAAC,SAAS,CAAC,iBAAiB,CAAC;SACzD,CAAC,CAAC,CAAC;QAEJ,OAAO;YACL,OAAO,EAAE,CAAC;oBACR,IAAI,EAAE,MAAe;oBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,cAAc,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;iBAC7D,CAAC;SACH,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-render-profile.d.ts","sourceRoot":"","sources":["../../src/tools/load-render-profile.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAOpE,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA2BjE"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { formatMs } from '../format.js';
|
|
5
|
+
import { parseRenderProfile } from '../parser/react-profile.js';
|
|
6
|
+
import { storeRenderProfile } from '../store.js';
|
|
7
|
+
export function registerLoadRenderProfile(server) {
|
|
8
|
+
server.registerTool('load_render_profile', {
|
|
9
|
+
title: 'Load Render Profile',
|
|
10
|
+
description: 'Parse and load a React DevTools profiler export captured from a Next.js app. Returns a profile ID for use with the other render analysis tools.',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
filePath: z.string().describe('Absolute or relative path to the exported React Profiler JSON file'),
|
|
13
|
+
},
|
|
14
|
+
}, async ({ filePath }) => {
|
|
15
|
+
const absPath = resolve(filePath);
|
|
16
|
+
const content = await readFile(absPath, 'utf-8');
|
|
17
|
+
const profile = parseRenderProfile(content, absPath);
|
|
18
|
+
storeRenderProfile(profile);
|
|
19
|
+
return {
|
|
20
|
+
content: [{
|
|
21
|
+
type: 'text',
|
|
22
|
+
text: JSON.stringify({
|
|
23
|
+
profileId: profile.id,
|
|
24
|
+
filename: profile.filename,
|
|
25
|
+
version: profile.version,
|
|
26
|
+
commitCount: profile.commits.length,
|
|
27
|
+
componentCount: profile.components.length,
|
|
28
|
+
totalCommitDuration: formatMs(profile.totalCommitDuration),
|
|
29
|
+
}, null, 2),
|
|
30
|
+
}],
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=load-render-profile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-render-profile.js","sourceRoot":"","sources":["../../src/tools/load-render-profile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,UAAU,yBAAyB,CAAC,MAAiB;IACzD,MAAM,CAAC,YAAY,CAAC,qBAAqB,EAAE;QACzC,KAAK,EAAE,qBAAqB;QAC5B,WAAW,EAAE,iJAAiJ;QAC9J,WAAW,EAAE;YACX,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,oEAAoE,CAAC;SACpG;KACF,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE;QACxB,MAAM,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;QAClC,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACjD,MAAM,OAAO,GAAG,kBAAkB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACrD,kBAAkB,CAAC,OAAO,CAAC,CAAC;QAE5B,OAAO;YACL,OAAO,EAAE,CAAC;oBACR,IAAI,EAAE,MAAe;oBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;wBACnB,SAAS,EAAE,OAAO,CAAC,EAAE;wBACrB,QAAQ,EAAE,OAAO,CAAC,QAAQ;wBAC1B,OAAO,EAAE,OAAO,CAAC,OAAO;wBACxB,WAAW,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM;wBACnC,cAAc,EAAE,OAAO,CAAC,UAAU,CAAC,MAAM;wBACzC,mBAAmB,EAAE,QAAQ,CAAC,OAAO,CAAC,mBAAmB,CAAC;qBAC3D,EAAE,IAAI,EAAE,CAAC,CAAC;iBACZ,CAAC;SACH,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@perfonext/render-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for analyzing Next.js React Profiler exports and render performance",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"perfonext-render-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc && chmod +x dist/index.js",
|
|
23
|
+
"dev": "tsc --watch",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"inspector": "npx @modelcontextprotocol/inspector node dist/index.js",
|
|
26
|
+
"prepublishOnly": "npm run build"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"mcp",
|
|
30
|
+
"github-copilot",
|
|
31
|
+
"nextjs",
|
|
32
|
+
"react-profiler",
|
|
33
|
+
"render",
|
|
34
|
+
"rerender",
|
|
35
|
+
"hydration",
|
|
36
|
+
"performance"
|
|
37
|
+
],
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/souvikxcoder/perfonext-render-mcp.git"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/souvikxcoder/perfonext-render-mcp/issues"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/souvikxcoder/perfonext-render-mcp#readme",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
|
+
"zod": "^4.4.1"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^25.6.0",
|
|
53
|
+
"typescript": "^6.0.3",
|
|
54
|
+
"vitest": "^3.2.4"
|
|
55
|
+
}
|
|
56
|
+
}
|