Skip to content

Commit 90a124a

Browse files
jorge-cabeps1lon
andauthored
[mdn] Initial experiment for adding performance tool (facebook#33045)
## Summary Add a way for the agent to get some data on the performance of react code ## How did you test this change? Tested function independently and directly with claude desktop app --------- Co-authored-by: Sebastian "Sebbie" Silbermann <[email protected]>
1 parent 49ea8bf commit 90a124a

File tree

4 files changed

+3048
-2482
lines changed

4 files changed

+3048
-2482
lines changed

compiler/packages/react-mcp-server/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,22 @@
1717
"@babel/parser": "^7.26",
1818
"@babel/plugin-syntax-typescript": "^7.25.9",
1919
"@modelcontextprotocol/sdk": "^1.9.0",
20+
"@types/jest": "^29.5.14",
2021
"algoliasearch": "^5.23.3",
2122
"cheerio": "^1.0.0",
2223
"html-to-text": "^9.0.5",
24+
"jest": "^29.7.0",
2325
"prettier": "^3.3.3",
26+
"puppeteer": "^24.7.2",
27+
"ts-jest": "^29.3.2",
2428
"zod": "^3.23.8"
2529
},
2630
"devDependencies": {
31+
"@babel/plugin-proposal-class-properties": "^7.18.6",
32+
"@babel/plugin-transform-runtime": "^7.26.10",
33+
"@babel/preset-env": "^7.26.9",
34+
"@babel/preset-react": "^7.26.3",
35+
"@babel/preset-typescript": "^7.27.0",
2736
"@types/html-to-text": "^9.0.4"
2837
},
2938
"license": "MIT",

compiler/packages/react-mcp-server/src/index.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import * as cheerio from 'cheerio';
2020
import {queryAlgolia} from './utils/algolia';
2121
import assertExhaustive from './utils/assertExhaustive';
2222
import {convert} from 'html-to-text';
23+
import {measurePerformance} from './utils/runtimePerf';
2324

2425
const server = new McpServer({
2526
name: 'React',
@@ -353,6 +354,104 @@ Server Components - Shift data-heavy logic to the server whenever possible. Brea
353354
],
354355
}));
355356

357+
server.tool(
358+
'review-react-runtime',
359+
'Review the runtime of the code and get performance data to evaluate the proposed solution, the react code that is passed into this tool MUST contain an App component.',
360+
{
361+
text: z.string(),
362+
},
363+
async ({text}) => {
364+
try {
365+
const iterations = 20;
366+
367+
let perfData = {
368+
renderTime: 0,
369+
webVitals: {
370+
cls: 0,
371+
lcp: 0,
372+
inp: 0,
373+
fid: 0,
374+
ttfb: 0,
375+
},
376+
reactProfilerMetrics: {
377+
id: 0,
378+
phase: 0,
379+
actualDuration: 0,
380+
baseDuration: 0,
381+
startTime: 0,
382+
commitTime: 0,
383+
},
384+
error: null,
385+
};
386+
387+
for (let i = 0; i < iterations; i++) {
388+
const performanceResults = await measurePerformance(text);
389+
perfData.renderTime += performanceResults.renderTime;
390+
perfData.webVitals.cls += performanceResults.webVitals.cls?.value || 0;
391+
perfData.webVitals.lcp += performanceResults.webVitals.lcp?.value || 0;
392+
perfData.webVitals.inp += performanceResults.webVitals.inp?.value || 0;
393+
perfData.webVitals.fid += performanceResults.webVitals.fid?.value || 0;
394+
perfData.webVitals.ttfb +=
395+
performanceResults.webVitals.ttfb?.value || 0;
396+
397+
perfData.reactProfilerMetrics.id +=
398+
performanceResults.reactProfilerMetrics.actualDuration?.value || 0;
399+
perfData.reactProfilerMetrics.phase +=
400+
performanceResults.reactProfilerMetrics.phase?.value || 0;
401+
perfData.reactProfilerMetrics.actualDuration +=
402+
performanceResults.reactProfilerMetrics.actualDuration?.value || 0;
403+
perfData.reactProfilerMetrics.baseDuration +=
404+
performanceResults.reactProfilerMetrics.baseDuration?.value || 0;
405+
perfData.reactProfilerMetrics.startTime +=
406+
performanceResults.reactProfilerMetrics.startTime?.value || 0;
407+
perfData.reactProfilerMetrics.commitTime +=
408+
performanceResults.reactProfilerMetrics.commitTim?.value || 0;
409+
}
410+
411+
const formattedResults = `
412+
# React Component Performance Results
413+
414+
## Mean Render Time
415+
${perfData.renderTime / iterations}ms
416+
417+
## Mean Web Vitals
418+
- Cumulative Layout Shift (CLS): ${perfData.webVitals.cls / iterations}
419+
- Largest Contentful Paint (LCP): ${perfData.webVitals.lcp / iterations}ms
420+
- Interaction to Next Paint (INP): ${perfData.webVitals.inp / iterations}ms
421+
- First Input Delay (FID): ${perfData.webVitals.fid / iterations}ms
422+
- Time to First Byte (TTFB): ${perfData.webVitals.ttfb / iterations}ms
423+
424+
## Mean React Profiler
425+
- Actual Duration: ${perfData.reactProfilerMetrics.actualDuration / iterations}ms
426+
- Base Duration: ${perfData.reactProfilerMetrics.baseDuration / iterations}ms
427+
- Start Time: ${perfData.reactProfilerMetrics.startTime / iterations}ms
428+
- Commit Time: ${perfData.reactProfilerMetrics.commitTime / iterations}ms
429+
430+
These metrics can help you evaluate the performance of your React component. Lower values generally indicate better performance.
431+
`;
432+
433+
return {
434+
content: [
435+
{
436+
type: 'text' as const,
437+
text: formattedResults,
438+
},
439+
],
440+
};
441+
} catch (error) {
442+
return {
443+
isError: true,
444+
content: [
445+
{
446+
type: 'text' as const,
447+
text: `Error measuring performance: ${error.message}\n\n${error.stack}`,
448+
},
449+
],
450+
};
451+
}
452+
},
453+
);
454+
356455
async function main() {
357456
const transport = new StdioServerTransport();
358457
await server.connect(transport);
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import * as babel from '@babel/core';
2+
import puppeteer from 'puppeteer';
3+
4+
export async function measurePerformance(code: any) {
5+
let options = {
6+
configFile: false,
7+
babelrc: false,
8+
presets: [['@babel/preset-env'], '@babel/preset-react'],
9+
};
10+
11+
const parsed = await babel.parseAsync(code, options);
12+
13+
if (!parsed) {
14+
throw new Error('Failed to parse code');
15+
}
16+
17+
const transpiled = await transformAsync(parsed);
18+
19+
if (!transpiled) {
20+
throw new Error('Failed to transpile code');
21+
}
22+
23+
const browser = await puppeteer.launch({
24+
protocolTimeout: 600_000,
25+
});
26+
27+
const page = await browser.newPage();
28+
await page.setViewport({width: 1280, height: 720});
29+
const html = buildHtml(transpiled);
30+
await page.setContent(html, {waitUntil: 'networkidle0'});
31+
32+
await page.waitForFunction(
33+
'window.__RESULT__ !== undefined && (window.__RESULT__.renderTime !== null || window.__RESULT__.error !== null)',
34+
{timeout: 600_000},
35+
);
36+
37+
const result = await page.evaluate(() => {
38+
return (window as any).__RESULT__;
39+
});
40+
41+
await browser.close();
42+
return result;
43+
}
44+
45+
/**
46+
* Transform AST into browser-compatible JavaScript
47+
* @param {babel.types.File} ast - The AST to transform
48+
* @param {Object} opts - Transformation options
49+
* @returns {Promise<string>} - The transpiled code
50+
*/
51+
async function transformAsync(ast: babel.types.Node) {
52+
const result = await babel.transformFromAstAsync(ast, undefined, {
53+
filename: 'file.jsx',
54+
presets: [['@babel/preset-env'], '@babel/preset-react'],
55+
plugins: [
56+
() => ({
57+
visitor: {
58+
ImportDeclaration(path: any) {
59+
const value = path.node.source.value;
60+
if (value === 'react' || value === 'react-dom') {
61+
path.remove();
62+
}
63+
},
64+
},
65+
}),
66+
],
67+
});
68+
69+
return result?.code || '';
70+
}
71+
72+
function buildHtml(transpiled: string) {
73+
const html = `
74+
<!DOCTYPE html>
75+
<html>
76+
<head>
77+
<meta charset="UTF-8">
78+
<title>React Performance Test</title>
79+
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
80+
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
81+
<script src="https://unpkg.com/[email protected]/dist/web-vitals.iife.js"></script>
82+
<style>
83+
body { margin: 0; }
84+
#root { padding: 20px; }
85+
</style>
86+
</head>
87+
<body>
88+
<div id="root"></div>
89+
<script>
90+
window.__RESULT__ = {
91+
renderTime: null,
92+
webVitals: {},
93+
reactProfilerMetrics: {},
94+
error: null
95+
};
96+
97+
webVitals.onCLS((metric) => { window.__RESULT__.webVitals.cls = metric; });
98+
webVitals.onLCP((metric) => { window.__RESULT__.webVitals.lcp = metric; });
99+
webVitals.onINP((metric) => { window.__RESULT__.webVitals.inp = metric; });
100+
webVitals.onFID((metric) => { window.__RESULT__.webVitals.fid = metric; });
101+
webVitals.onTTFB((metric) => { window.__RESULT__.webVitals.ttfb = metric; });
102+
103+
try {
104+
${transpiled}
105+
106+
window.App = App;
107+
108+
// Render the component to the DOM with profiling
109+
const AppComponent = window.App || (() => React.createElement('div', null, 'No App component exported'));
110+
111+
const root = ReactDOM.createRoot(document.getElementById('root'), {
112+
onUncaughtError: (error, errorInfo) => {
113+
window.__RESULT__.error = error;
114+
}
115+
});
116+
117+
const renderStart = performance.now()
118+
119+
root.render(
120+
React.createElement(React.Profiler, {
121+
id: 'App',
122+
onRender: (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
123+
window.__RESULT__.reactProfilerMetrics.id = id;
124+
window.__RESULT__.reactProfilerMetrics.phase = phase;
125+
window.__RESULT__.reactProfilerMetrics.actualDuration = actualDuration;
126+
window.__RESULT__.reactProfilerMetrics.baseDuration = baseDuration;
127+
window.__RESULT__.reactProfilerMetrics.startTime = startTime;
128+
window.__RESULT__.reactProfilerMetrics.commitTime = commitTime;
129+
}
130+
}, React.createElement(AppComponent))
131+
);
132+
133+
const renderEnd = performance.now();
134+
135+
window.__RESULT__.renderTime = renderEnd - renderStart;
136+
} catch (error) {
137+
console.error('Error rendering component:', error);
138+
window.__RESULT__.error = {
139+
message: error.message,
140+
stack: error.stack
141+
};
142+
}
143+
</script>
144+
<script>
145+
window.onerror = function(message, url, lineNumber) {
146+
window.__RESULT__.error = message;
147+
};
148+
</script>
149+
</body>
150+
</html>
151+
`;
152+
153+
return html;
154+
}

0 commit comments

Comments
 (0)