Skip to content

[mdn] Initial experiment for adding performance tool #33045

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Apr 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7894131
[mdn] runtimePerf tool initial commit
jorge-cab Apr 28, 2025
6733b9b
Merge branch 'main' of https://github.com/jorge-cab/react
jorge-cab Apr 28, 2025
43370f6
Merge branch 'main' of https://github.com/jorge-cab/react
jorge-cab Apr 28, 2025
609b1f9
Merge branch 'main' of https://github.com/jorge-cab/react
jorge-cab Apr 28, 2025
c3f2408
Merge branch 'main' of https://github.com/jorge-cab/react
jorge-cab Apr 28, 2025
8af2d22
Merge branch 'main' of https://github.com/jorge-cab/react
jorge-cab Apr 28, 2025
29d4c50
[devtools] Allow inspecting cause, name, message, stack of Errors in …
eps1lon Apr 26, 2025
1a79060
Merge branch 'main' of https://github.com/jorge-cab/react
jorge-cab Apr 28, 2025
98d11e2
Merge branch 'facebook:main' into main
jorge-cab Apr 28, 2025
082829c
Merge branch 'facebook:main' into main
jorge-cab Apr 28, 2025
b773add
Merge branch 'main' of https://github.com/jorge-cab/react into HEAD
jorge-cab Apr 29, 2025
3d64fcb
Merge branch 'main' of https://github.com/jorge-cab/react into HEAD
jorge-cab Apr 29, 2025
c6d9e4e
Merge branch 'main' of https://github.com/jorge-cab/react into HEAD
jorge-cab Apr 30, 2025
2776fca
Merge branch 'main' of https://github.com/jorge-cab/react into HEAD
jorge-cab Apr 30, 2025
6b3e24f
Merge branch 'main' of https://github.com/jorge-cab/react into HEAD
jorge-cab Apr 30, 2025
4c9cde2
Merge branch 'main' of https://github.com/jorge-cab/react into HEAD
jorge-cab Apr 30, 2025
2d85dea
Merge branch 'main' of https://github.com/jorge-cab/react into HEAD
jorge-cab Apr 30, 2025
5350a13
Merge branch 'main' of https://github.com/jorge-cab/react into HEAD
jorge-cab Apr 30, 2025
e45b6f8
Remove test
jorge-cab Apr 30, 2025
ed4665c
Clean test and formatting
jorge-cab Apr 30, 2025
b6e447c
Merge branch 'main' of https://github.com/jorge-cab/react
jorge-cab Apr 30, 2025
156bd46
Merge branch 'main' of https://github.com/jorge-cab/react
jorge-cab Apr 30, 2025
32cccb4
Merge branch 'main' of https://github.com/jorge-cab/react
jorge-cab Apr 30, 2025
ee7368d
Remove jest.config.js
jorge-cab Apr 30, 2025
ee05ffc
Move window.App definition to template and add context to tool descri…
jorge-cab Apr 30, 2025
d1f33b6
Merge branch 'main' into main
jorge-cab Apr 30, 2025
6e38fc0
Increase to 20 iterations
jorge-cab Apr 30, 2025
a6a8850
Merge branch 'main' of https://github.com/jorge-cab/react
jorge-cab Apr 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions compiler/packages/react-mcp-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
99 changes: 99 additions & 0 deletions compiler/packages/react-mcp-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nice touch. Could you try asking it to run this tool multiple times to see if it improves on these metrics?

One more thing we might want to do is to run several iterations of the test in a single tool call to reduce noise. Just using the mean for now seems like a good start.

`;

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);
Expand Down
154 changes: 154 additions & 0 deletions compiler/packages/react-mcp-server/src/utils/runtimePerf.ts
Original file line number Diff line number Diff line change
@@ -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},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small note but i don't think this timeout needs to be that long. If it takes over 10 seconds something is probably wrong. Feel free to address this in a follow up PR though.

);

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 = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

message: error.message,
stack: error.stack
};
}
</script>
<script>
window.onerror = function(message, url, lineNumber) {
window.__RESULT__.error = message;
};
</script>
</body>
</html>
`;

return html;
}
Loading
Loading