Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
5 changes: 5 additions & 0 deletions compiler/packages/react-mcp-server/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
};
11 changes: 10 additions & 1 deletion compiler/packages/react-mcp-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
},
"scripts": {
"build": "rimraf dist && tsup",
"test": "echo 'no tests'",
"test": "jest",
"dev": "concurrently --kill-others -n build,inspect \"yarn run watch\" \"wait-on dist/index.js && yarn run inspect\"",
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js",
"watch": "yarn build --watch"
Expand All @@ -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
45 changes: 45 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,50 @@ 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', {
text: z.string(),
},
async ({text}) => {
try {
const performanceResults = await measurePerformance(text);

const formattedResults = `
# React Component Performance Results

## Render Time
${performanceResults.renderTime.toFixed(2)}ms

## Web Vitals
- Cumulative Layout Shift (CLS): ${performanceResults.webVitals.cls?.value || 'N/A'}
- Largest Contentful Paint (LCP): ${performanceResults.webVitals.lcp?.value || 'N/A'}ms
- Interaction to Next Paint (INP): ${performanceResults.webVitals.inp?.value || 'N/A'}ms
- First Input Delay (FID): ${performanceResults.webVitals.fid?.value || 'N/A'}ms
- Time to First Byte (TTFB): ${performanceResults.webVitals.ttfb?.value || 'N/A'}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
147 changes: 147 additions & 0 deletions compiler/packages/react-mcp-server/src/tests/runtimePerf.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { measurePerformance } from '../utils/runtimePerf';
import puppeteer from 'puppeteer';

describe('runtimePerf', () => {
describe('measurePerformance', () => {
it('should measure a basic React component', async () => {
const sampleCode = `
import React from 'react';
function App() {
return <div>Hello World</div>;
}
window.App = App;
`;

const result = await measurePerformance(sampleCode);

expect(result).toHaveProperty('renderTime');
expect(result).toHaveProperty('webVitals');
console.log(result);
Copy link
Member

Choose a reason for hiding this comment

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

nit: these console.logs can be removed before merging, they tend to clutter output

}, 300000);

it('should handle components with state', async () => {
const complexCode = `
import React, { useState } from 'react';
export function App() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Counter: {count}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
window.App = App;
`;

const result = await measurePerformance(complexCode);

expect(result).toHaveProperty('renderTime');
expect(result).toHaveProperty('webVitals');
console.log(result);
}, 300000);

it('should measure a component with heavy rendering load', async () => {
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this test adds much value outside of slowing everything down. It might be worth having tests for the unhappy paths, for example where there's invalid syntax, imports to some unknown/non-present module, etc

Great job adding tests btw!

const heavyRenderCode = `
import React, { useState, useMemo } from 'react';
// Complex calculation function
function calculatePrimes(max) {
const sieve = Array(max).fill(true);
sieve[0] = sieve[1] = false;
for (let i = 2; i <= Math.sqrt(max); i++) {
if (sieve[i]) {
for (let j = i * i; j < max; j += i) {
sieve[j] = false;
}
}
}
return Array.from({ length: max }, (_, i) => i).filter(i => sieve[i]);
}
// Recursive Fibonacci - intentionally inefficient
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// Complex nested component
function ComplexItem({ index, depth = 0 }) {
// Calculate something expensive for each item
const fib = React.useMemo(() => fibonacci(depth + 10), [depth]);
if (depth > 3) {
return (
<div className="item-wrapper" style={{ padding: depth * 5 }}>
<div className="item">
Item {index} (Depth: {depth}, Fibonacci: {fib})
</div>
</div>
);
}
return (
<div className="item-wrapper" style={{ padding: depth * 5 }}>
<div className="item">
Item {index} (Depth: {depth}, Fibonacci: {fib})
{Array.from({ length: 3 }, (_, i) => (
<ComplexItem key={i} index={index + '-' + i} depth={depth + 1} />
))}
</div>
</div>
);
}
export function App() {
const [items, setItems] = React.useState(50);
// Calculate prime numbers on render - computationally expensive
const primes = React.useMemo(() => calculatePrimes(1000), []);
return (
<div className="heavy-render-app">
<h1>Heavy Rendering Test</h1>
<p>Found {primes.length} prime numbers</p>
<div className="controls">
<button onClick={() => setItems(prev => Math.max(10, prev - 10))}>
Fewer Items
</button>
<span> {items} items </span>
<button onClick={() => setItems(prev => prev + 10)}>
More Items
</button>
</div>
<div className="item-list" style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
gap: '10px'
}}>
{Array.from({ length: items }, (_, i) => (
<ComplexItem key={i} index={i} />
))}
</div>
</div>
);
}
window.App = App;
`;

const result = await measurePerformance(heavyRenderCode);

expect(result).toHaveProperty('renderTime');
expect(result).toHaveProperty('webVitals');
expect(result.renderTime).toBeGreaterThan(10); // Expect rendering to take more than 10ms
console.log('Heavy render performance:', result);
}, 600000); // Longer timeout for heavy rendering
});
});
138 changes: 138 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,138 @@
import * as babel from '@babel/core';
import * as parser from '@babel/parser';
import puppeteer from 'puppeteer';

export async function measurePerformance(code: any) {
// Parse the code into an AST
const parsed = await parseAsync(code);

// Transform the AST into browser-compatible JavaScript
const transpiled = await transformAsync(parsed, code);

console.log(transpiled);

// Launch puppeteer with increased protocol timeout
const browser = await puppeteer.launch({
protocolTimeout: 600_000, // Increase timeout to 10 minutes
headless: false
Copy link
Member

Choose a reason for hiding this comment

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

Probably fine for debugging, but seems like this would be less irritating for the user if it was headless?

});
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', {timeout: 600_000}); // 10 minute timeout

const result = await page.evaluate(() => {
return (window as any).__RESULT__;
});
// await browser.close();
return result;
}

/**
* Parse code string into an AST
* @param {string} code - The source code to parse
* @returns {babel.types.File} - The parsed AST
*/
function parseAsync(code: string) {
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 already exported from babel: https://babeljs.io/docs/babel-core#parseasync. You'll also probably want to use @babel/preset-react and also tune the settings so its maximally compatible with all types of syntax since generally since we have no control over what the user is going to paste in. Might also be good to return a helpful error message if it couldn't parse / transform, since it can help the LLM adjust the syntax and try again.

One more thing you'll want to add is

  configFile: false,
  babelrc: false,

as otherwise Babel will try to look for a babel.config.js somewhere and it can throw things off.

These config options can also be shared with transformFromAstAsync I believe.

return parser.parse(code, {
sourceType: 'module',
plugins: [
'jsx',
'typescript',
'classProperties',
'optionalChaining',
'nullishCoalescingOperator',
],
});
}

/**
* 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, code: string) {
// Provide a dummy filename to satisfy Babel's requirement for filename context
const result = await babel.transformFromAstAsync(ast, null, {
filename: 'file.jsx',
presets: [
['@babel/preset-env'],
'@babel/preset-react'
],
plugins: [
() => ({
visitor: {
ImportDeclaration(path) {
const value = path.node.source.value;
if (value === 'react' || value === 'react-dom') {
path.remove();
}
}
}
})
]
});

return result?.code || '';
}

function buildHtml(transpiled: string | null | undefined) {
Copy link
Member

Choose a reason for hiding this comment

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

This argument really should be non-nullable. There is currently no handling of the null/undefined case here. I would move this check into the callsite instead and throw early if transpiled is ever null/undefined since there's nothing to do if the code couldn't be transpiled.

// Create HTML that includes React, ReactDOM, and the transpiled code
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/[email protected]/dist/web-vitals.iife.js"></script>
<style>
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; }
#root { padding: 20px; }
</style>
</head>
<body>
<div id="root"></div>
<script type="module">
// Store performance metrics
window.__RESULT__ = {
renderTime: null,
webVitals: {}
};
// Measure web vitals
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; });
// Wrap user code in React.Profiler to measure render performance
const renderStart = performance.now();
Copy link
Member

Choose a reason for hiding this comment

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

where's the renderEnd?

Copy link
Member

Choose a reason for hiding this comment

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

also this seems unnecessary given that React.Profiler already gives you the time taken to mount.

// Execute the transpiled code
${transpiled}
// 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'));
console.log('rendering...');
root.render(
React.createElement(React.Profiler, {
id: 'App',
onRender: (id, phase, actualDuration) => {
window.__RESULT__.renderTime = actualDuration;
Copy link
Member

Choose a reason for hiding this comment

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

could probably capture all the arguments

}
}, React.createElement(AppComponent))
);
</script>
</body>
</html>
`;

return html;
}
Loading
Loading