[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:
Jorge Cabiedes 2025-04-30 12:44:05 -07:00 committed by GitHub
parent 49ea8bf569
commit 90a124a980
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 3089 additions and 2523 deletions

View File

@ -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",

View File

@ -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);

View 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;
}

File diff suppressed because it is too large Load Diff