Repository: facebook/react
Pull request: [mdn] Initial experiment for adding performance tool

## Summary
Add a way for the agent to get some data on the performance of react code

## How did you test this change?
Unit tests

Changed files:
filename: compiler/packages/react-mcp-server/package.json
patch: @@ -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",

filename: compiler/packages/react-mcp-server/src/index.ts
patch: @@ -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);

filename: compiler/packages/react-mcp-server/src/utils/runtimePerf.ts
patch: @@ -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;
+}