mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
[mdn] Initial experiment for adding performance tool (#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 <sebastian.silbermann@vercel.com>
This commit is contained in:
parent
49ea8bf569
commit
90a124a980
|
|
@ -17,13 +17,22 @@
|
|||
"@babel/parser": "^7.26",
|
||||
"@babel/plugin-syntax-typescript": "^7.25.9",
|
||||
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"algoliasearch": "^5.23.3",
|
||||
"cheerio": "^1.0.0",
|
||||
"html-to-text": "^9.0.5",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.3.3",
|
||||
"puppeteer": "^24.7.2",
|
||||
"ts-jest": "^29.3.2",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-transform-runtime": "^7.26.10",
|
||||
"@babel/preset-env": "^7.26.9",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@babel/preset-typescript": "^7.27.0",
|
||||
"@types/html-to-text": "^9.0.4"
|
||||
},
|
||||
"license": "MIT",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import * as cheerio from 'cheerio';
|
|||
import {queryAlgolia} from './utils/algolia';
|
||||
import assertExhaustive from './utils/assertExhaustive';
|
||||
import {convert} from 'html-to-text';
|
||||
import {measurePerformance} from './utils/runtimePerf';
|
||||
|
||||
const server = new McpServer({
|
||||
name: 'React',
|
||||
|
|
@ -353,6 +354,104 @@ Server Components - Shift data-heavy logic to the server whenever possible. Brea
|
|||
],
|
||||
}));
|
||||
|
||||
server.tool(
|
||||
'review-react-runtime',
|
||||
'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.',
|
||||
{
|
||||
text: z.string(),
|
||||
},
|
||||
async ({text}) => {
|
||||
try {
|
||||
const iterations = 20;
|
||||
|
||||
let perfData = {
|
||||
renderTime: 0,
|
||||
webVitals: {
|
||||
cls: 0,
|
||||
lcp: 0,
|
||||
inp: 0,
|
||||
fid: 0,
|
||||
ttfb: 0,
|
||||
},
|
||||
reactProfilerMetrics: {
|
||||
id: 0,
|
||||
phase: 0,
|
||||
actualDuration: 0,
|
||||
baseDuration: 0,
|
||||
startTime: 0,
|
||||
commitTime: 0,
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const performanceResults = await measurePerformance(text);
|
||||
perfData.renderTime += performanceResults.renderTime;
|
||||
perfData.webVitals.cls += performanceResults.webVitals.cls?.value || 0;
|
||||
perfData.webVitals.lcp += performanceResults.webVitals.lcp?.value || 0;
|
||||
perfData.webVitals.inp += performanceResults.webVitals.inp?.value || 0;
|
||||
perfData.webVitals.fid += performanceResults.webVitals.fid?.value || 0;
|
||||
perfData.webVitals.ttfb +=
|
||||
performanceResults.webVitals.ttfb?.value || 0;
|
||||
|
||||
perfData.reactProfilerMetrics.id +=
|
||||
performanceResults.reactProfilerMetrics.actualDuration?.value || 0;
|
||||
perfData.reactProfilerMetrics.phase +=
|
||||
performanceResults.reactProfilerMetrics.phase?.value || 0;
|
||||
perfData.reactProfilerMetrics.actualDuration +=
|
||||
performanceResults.reactProfilerMetrics.actualDuration?.value || 0;
|
||||
perfData.reactProfilerMetrics.baseDuration +=
|
||||
performanceResults.reactProfilerMetrics.baseDuration?.value || 0;
|
||||
perfData.reactProfilerMetrics.startTime +=
|
||||
performanceResults.reactProfilerMetrics.startTime?.value || 0;
|
||||
perfData.reactProfilerMetrics.commitTime +=
|
||||
performanceResults.reactProfilerMetrics.commitTim?.value || 0;
|
||||
}
|
||||
|
||||
const formattedResults = `
|
||||
# React Component Performance Results
|
||||
|
||||
## Mean Render Time
|
||||
${perfData.renderTime / iterations}ms
|
||||
|
||||
## Mean Web Vitals
|
||||
- Cumulative Layout Shift (CLS): ${perfData.webVitals.cls / iterations}
|
||||
- Largest Contentful Paint (LCP): ${perfData.webVitals.lcp / iterations}ms
|
||||
- Interaction to Next Paint (INP): ${perfData.webVitals.inp / iterations}ms
|
||||
- First Input Delay (FID): ${perfData.webVitals.fid / iterations}ms
|
||||
- Time to First Byte (TTFB): ${perfData.webVitals.ttfb / iterations}ms
|
||||
|
||||
## Mean React Profiler
|
||||
- Actual Duration: ${perfData.reactProfilerMetrics.actualDuration / iterations}ms
|
||||
- Base Duration: ${perfData.reactProfilerMetrics.baseDuration / iterations}ms
|
||||
- Start Time: ${perfData.reactProfilerMetrics.startTime / iterations}ms
|
||||
- Commit Time: ${perfData.reactProfilerMetrics.commitTime / iterations}ms
|
||||
|
||||
These metrics can help you evaluate the performance of your React component. Lower values generally indicate better performance.
|
||||
`;
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: formattedResults,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: `Error measuring performance: ${error.message}\n\n${error.stack}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
|
|
|
|||
154
compiler/packages/react-mcp-server/src/utils/runtimePerf.ts
Normal file
154
compiler/packages/react-mcp-server/src/utils/runtimePerf.ts
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
import * as babel from '@babel/core';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
export async function measurePerformance(code: any) {
|
||||
let options = {
|
||||
configFile: false,
|
||||
babelrc: false,
|
||||
presets: [['@babel/preset-env'], '@babel/preset-react'],
|
||||
};
|
||||
|
||||
const parsed = await babel.parseAsync(code, options);
|
||||
|
||||
if (!parsed) {
|
||||
throw new Error('Failed to parse code');
|
||||
}
|
||||
|
||||
const transpiled = await transformAsync(parsed);
|
||||
|
||||
if (!transpiled) {
|
||||
throw new Error('Failed to transpile code');
|
||||
}
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
protocolTimeout: 600_000,
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({width: 1280, height: 720});
|
||||
const html = buildHtml(transpiled);
|
||||
await page.setContent(html, {waitUntil: 'networkidle0'});
|
||||
|
||||
await page.waitForFunction(
|
||||
'window.__RESULT__ !== undefined && (window.__RESULT__.renderTime !== null || window.__RESULT__.error !== null)',
|
||||
{timeout: 600_000},
|
||||
);
|
||||
|
||||
const result = await page.evaluate(() => {
|
||||
return (window as any).__RESULT__;
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform AST into browser-compatible JavaScript
|
||||
* @param {babel.types.File} ast - The AST to transform
|
||||
* @param {Object} opts - Transformation options
|
||||
* @returns {Promise<string>} - The transpiled code
|
||||
*/
|
||||
async function transformAsync(ast: babel.types.Node) {
|
||||
const result = await babel.transformFromAstAsync(ast, undefined, {
|
||||
filename: 'file.jsx',
|
||||
presets: [['@babel/preset-env'], '@babel/preset-react'],
|
||||
plugins: [
|
||||
() => ({
|
||||
visitor: {
|
||||
ImportDeclaration(path: any) {
|
||||
const value = path.node.source.value;
|
||||
if (value === 'react' || value === 'react-dom') {
|
||||
path.remove();
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
return result?.code || '';
|
||||
}
|
||||
|
||||
function buildHtml(transpiled: string) {
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>React Performance Test</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
|
||||
<script src="https://unpkg.com/web-vitals@3.0.0/dist/web-vitals.iife.js"></script>
|
||||
<style>
|
||||
body { margin: 0; }
|
||||
#root { padding: 20px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script>
|
||||
window.__RESULT__ = {
|
||||
renderTime: null,
|
||||
webVitals: {},
|
||||
reactProfilerMetrics: {},
|
||||
error: null
|
||||
};
|
||||
|
||||
webVitals.onCLS((metric) => { window.__RESULT__.webVitals.cls = metric; });
|
||||
webVitals.onLCP((metric) => { window.__RESULT__.webVitals.lcp = metric; });
|
||||
webVitals.onINP((metric) => { window.__RESULT__.webVitals.inp = metric; });
|
||||
webVitals.onFID((metric) => { window.__RESULT__.webVitals.fid = metric; });
|
||||
webVitals.onTTFB((metric) => { window.__RESULT__.webVitals.ttfb = metric; });
|
||||
|
||||
try {
|
||||
${transpiled}
|
||||
|
||||
window.App = App;
|
||||
|
||||
// Render the component to the DOM with profiling
|
||||
const AppComponent = window.App || (() => React.createElement('div', null, 'No App component exported'));
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'), {
|
||||
onUncaughtError: (error, errorInfo) => {
|
||||
window.__RESULT__.error = error;
|
||||
}
|
||||
});
|
||||
|
||||
const renderStart = performance.now()
|
||||
|
||||
root.render(
|
||||
React.createElement(React.Profiler, {
|
||||
id: 'App',
|
||||
onRender: (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
|
||||
window.__RESULT__.reactProfilerMetrics.id = id;
|
||||
window.__RESULT__.reactProfilerMetrics.phase = phase;
|
||||
window.__RESULT__.reactProfilerMetrics.actualDuration = actualDuration;
|
||||
window.__RESULT__.reactProfilerMetrics.baseDuration = baseDuration;
|
||||
window.__RESULT__.reactProfilerMetrics.startTime = startTime;
|
||||
window.__RESULT__.reactProfilerMetrics.commitTime = commitTime;
|
||||
}
|
||||
}, React.createElement(AppComponent))
|
||||
);
|
||||
|
||||
const renderEnd = performance.now();
|
||||
|
||||
window.__RESULT__.renderTime = renderEnd - renderStart;
|
||||
} catch (error) {
|
||||
console.error('Error rendering component:', error);
|
||||
window.__RESULT__.error = {
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
window.onerror = function(message, url, lineNumber) {
|
||||
window.__RESULT__.error = message;
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
return html;
|
||||
}
|
||||
5350
compiler/yarn.lock
5350
compiler/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user