[mcp] Refactor (#33085)

Just some cleanup. Mainly, we now take the number of iterations as an
argument. Everything else is just code movement and small tweaks.
This commit is contained in:
lauren 2025-05-02 14:15:12 -04:00 committed by GitHub
parent b5450b0738
commit dc2b11817b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 234 additions and 205 deletions

View File

@ -275,6 +275,83 @@ server.tool(
}, },
); );
server.tool(
'review-react-runtime',
`Run this tool every time you propose a performance related change to verify if your suggestion actually improves performance.
<requirements>
This tool has some requirements on the code input:
- The react code that is passed into this tool MUST contain an App functional component without arrow function.
- DO NOT export anything since we can't parse export syntax with this tool.
- Only import React from 'react' and use all hooks and imports using the React. prefix like React.useState and React.useEffect
</requirements>
<goals>
- LCP - loading speed: good 2.5 s, needs-improvement 2.5-4 s, poor > 4 s
- INP - input responsiveness: good 200 ms, needs-improvement 200-500 ms, poor > 500 ms
- CLS - visual stability: good 0.10, needs-improvement 0.10-0.25, poor > 0.25
- (Optional: FCP 1.8 s, TTFB 0.8 s)
</goals>
<evaluation>
Classify each metric with the thresholds above. Identify the worst category in the order poor > needs-improvement > good.
</evaluation>
<iterate>
(repeat until every metric is good or two consecutive cycles show no gain)
- Apply one focused change based on the failing metric plus React-specific guidance:
- LCP: lazy-load off-screen images, inline critical CSS, preconnect, use React.lazy + Suspense for below-the-fold modules. if the user requests for it, use React Server Components for static content (Server Components).
- INP: wrap non-critical updates in useTransition, avoid calling setState inside useEffect.
- CLS: reserve space via explicit width/height or aspect-ratio, keep stable list keys, use fixed-size skeleton loaders, animate only transform/opacity, avoid inserting ads or banners without placeholders.
Stop when every metric is classified as good. Return the final metric table and the list of applied changes.
</iterate>
`,
{
text: z.string(),
iterations: z.number().optional().default(2),
},
async ({text, iterations}) => {
try {
const results = await measurePerformance(text, iterations);
const formattedResults = `
# React Component Performance Results
## Mean Render Time
${results.renderTime / iterations}ms
## Mean Web Vitals
- Cumulative Layout Shift (CLS): ${results.webVitals.cls / iterations}ms
- Largest Contentful Paint (LCP): ${results.webVitals.lcp / iterations}ms
- Interaction to Next Paint (INP): ${results.webVitals.inp / iterations}ms
- First Input Delay (FID): ${results.webVitals.fid / iterations}ms
## Mean React Profiler
- Actual Duration: ${results.reactProfiler.actualDuration / iterations}ms
- Base Duration: ${results.reactProfiler.baseDuration / iterations}ms
`;
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}`,
},
],
};
}
},
);
server.prompt('review-react-code', () => ({ server.prompt('review-react-code', () => ({
messages: [ messages: [
{ {
@ -354,129 +431,6 @@ Server Components - Shift data-heavy logic to the server whenever possible. Brea
], ],
})); }));
server.tool(
'review-react-runtime',
`Run this tool every time you propose a performance related change to verify if your suggestion actually improves performance.
<requirements>
This tool has some requirements on the code input:
- The react code that is passed into this tool MUST contain an App functional component without arrow function.
- DO NOT export anything since we can't parse export syntax with this tool.
- Only import React from 'react' and use all hooks and imports using the React. prefix like React.useState and React.useEffect
</requirements>
<goals>
- LCP - loading speed: good 2.5 s, needs-improvement 2.5-4 s, poor > 4 s
- INP - input responsiveness: good 200 ms, needs-improvement 200-500 ms, poor > 500 ms
- CLS - visual stability: good 0.10, needs-improvement 0.10-0.25, poor > 0.25
- (Optional: FCP 1.8 s, TTFB 0.8 s)
</goals>
<evaluation>
Classify each metric with the thresholds above. Identify the worst category in the order poor > needs-improvement > good.
</evaluation>
<iterate>
(repeat until every metric is good or two consecutive cycles show no gain)
- Apply one focused change based on the failing metric plus React-specific guidance:
- LCP: lazy-load off-screen images, inline critical CSS, preconnect, use React.lazy + Suspense for below-the-fold modules. if the user requests for it, use React Server Components for static content (Server Components).
- INP: wrap non-critical updates in useTransition, avoid calling setState inside useEffect.
- CLS: reserve space via explicit width/height or aspect-ratio, keep stable list keys, use fixed-size skeleton loaders, animate only transform/opacity, avoid inserting ads or banners without placeholders.
Stop when every metric is classified as good. Return the final metric table and the list of applied changes.
</iterate>
`,
{
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 || 0;
perfData.webVitals.lcp += performanceResults.webVitals.lcp || 0;
perfData.webVitals.inp += performanceResults.webVitals.inp || 0;
perfData.webVitals.fid += performanceResults.webVitals.fid || 0;
perfData.webVitals.ttfb += performanceResults.webVitals.ttfb || 0;
perfData.reactProfilerMetrics.id +=
performanceResults.reactProfilerMetrics.actualDuration || 0;
perfData.reactProfilerMetrics.phase +=
performanceResults.reactProfilerMetrics.phase || 0;
perfData.reactProfilerMetrics.actualDuration +=
performanceResults.reactProfilerMetrics.actualDuration || 0;
perfData.reactProfilerMetrics.baseDuration +=
performanceResults.reactProfilerMetrics.baseDuration || 0;
perfData.reactProfilerMetrics.startTime +=
performanceResults.reactProfilerMetrics.startTime || 0;
perfData.reactProfilerMetrics.commitTime +=
performanceResults.reactProfilerMetrics.commitTime || 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
`;
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() { async function main() {
const transport = new StdioServerTransport(); const transport = new StdioServerTransport();
await server.connect(transport); await server.connect(transport);

View File

@ -1,8 +1,32 @@
import * as babel from '@babel/core'; import * as babel from '@babel/core';
import puppeteer from 'puppeteer'; import puppeteer from 'puppeteer';
export async function measurePerformance(code: string) { type PerformanceResults = {
renderTime: number;
webVitals: {
cls: number;
lcp: number;
inp: number;
fid: number;
ttfb: number;
};
reactProfiler: {
id: number;
phase: number;
actualDuration: number;
baseDuration: number;
startTime: number;
commitTime: number;
};
error: Error | null;
};
export async function measurePerformance(
code: string,
iterations: number,
): Promise<PerformanceResults> {
const babelOptions = { const babelOptions = {
filename: 'anonymous.tsx',
configFile: false, configFile: false,
babelrc: false, babelrc: false,
presets: [ presets: [
@ -12,16 +36,13 @@ export async function measurePerformance(code: string) {
], ],
}; };
// Parse the code to AST
const parsed = await babel.parseAsync(code, babelOptions); const parsed = await babel.parseAsync(code, babelOptions);
if (!parsed) { if (!parsed) {
throw new Error('Failed to parse code'); throw new Error('Failed to parse code');
} }
// Transform AST to browser-compatible JavaScript
const transformResult = await babel.transformFromAstAsync(parsed, undefined, { const transformResult = await babel.transformFromAstAsync(parsed, undefined, {
...babelOptions, ...babelOptions,
filename: 'file.jsx',
plugins: [ plugins: [
() => ({ () => ({
visitor: { visitor: {
@ -44,104 +65,158 @@ export async function measurePerformance(code: string) {
} }
const browser = await puppeteer.launch(); const browser = await puppeteer.launch();
const page = await browser.newPage(); const page = await browser.newPage();
await page.setViewport({width: 1280, height: 720}); await page.setViewport({width: 1280, height: 720});
const html = buildHtml(transpiled); const html = buildHtml(transpiled);
await page.setContent(html, {waitUntil: 'networkidle0'});
await page.waitForFunction( let performanceResults: PerformanceResults = {
'window.__RESULT__ !== undefined && (window.__RESULT__.renderTime !== null || window.__RESULT__.error !== null)', renderTime: 0,
); webVitals: {
cls: 0,
lcp: 0,
inp: 0,
fid: 0,
ttfb: 0,
},
reactProfiler: {
id: 0,
phase: 0,
actualDuration: 0,
baseDuration: 0,
startTime: 0,
commitTime: 0,
},
error: null,
};
const result = await page.evaluate(() => { for (let ii = 0; ii < iterations; ii++) {
return (window as any).__RESULT__; await page.setContent(html, {waitUntil: 'networkidle0'});
}); await page.waitForFunction(
'window.__RESULT__ !== undefined && (window.__RESULT__.renderTime !== null || window.__RESULT__.error !== null)',
);
// ui chaos monkey
await page.waitForFunction(`window.__RESULT__ !== undefined && (function() {
for (const el of [...document.querySelectorAll('a'), ...document.querySelectorAll('button')]) {
console.log(el);
el.click();
}
return true;
})() `);
const evaluationResult: PerformanceResults = await page.evaluate(() => {
return (window as any).__RESULT__;
});
// TODO: investigate why webvital metrics are not populating correctly
performanceResults.renderTime += evaluationResult.renderTime;
performanceResults.webVitals.cls += evaluationResult.webVitals.cls || 0;
performanceResults.webVitals.lcp += evaluationResult.webVitals.lcp || 0;
performanceResults.webVitals.inp += evaluationResult.webVitals.inp || 0;
performanceResults.webVitals.fid += evaluationResult.webVitals.fid || 0;
performanceResults.webVitals.ttfb += evaluationResult.webVitals.ttfb || 0;
performanceResults.reactProfiler.id +=
evaluationResult.reactProfiler.actualDuration || 0;
performanceResults.reactProfiler.phase +=
evaluationResult.reactProfiler.phase || 0;
performanceResults.reactProfiler.actualDuration +=
evaluationResult.reactProfiler.actualDuration || 0;
performanceResults.reactProfiler.baseDuration +=
evaluationResult.reactProfiler.baseDuration || 0;
performanceResults.reactProfiler.startTime +=
evaluationResult.reactProfiler.startTime || 0;
performanceResults.reactProfiler.commitTime +=
evaluationResult.reactProfiler.commitTime || 0;
performanceResults.error = evaluationResult.error;
}
await browser.close(); await browser.close();
return result;
return performanceResults;
} }
function buildHtml(transpiled: string) { function buildHtml(transpiled: string) {
const html = ` const html = `
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>React Performance Test</title> <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@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.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> <script src="https://unpkg.com/web-vitals@3.0.0/dist/web-vitals.iife.js"></script>
<style> <style>
body { margin: 0; } body { margin: 0; }
#root { padding: 20px; } #root { padding: 20px; }
</style> </style>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script> <script>
window.__RESULT__ = { window.__RESULT__ = {
renderTime: null, renderTime: null,
webVitals: {}, webVitals: {},
reactProfilerMetrics: {}, reactProfiler: {},
error: null error: null
}; };
webVitals.onCLS((metric) => { window.__RESULT__.webVitals.cls = metric; }); webVitals.onCLS((metric) => { window.__RESULT__.webVitals.cls = metric; });
webVitals.onLCP((metric) => { window.__RESULT__.webVitals.lcp = metric; }); webVitals.onLCP((metric) => { window.__RESULT__.webVitals.lcp = metric; });
webVitals.onINP((metric) => { window.__RESULT__.webVitals.inp = metric; }); webVitals.onINP((metric) => { window.__RESULT__.webVitals.inp = metric; });
webVitals.onFID((metric) => { window.__RESULT__.webVitals.fid = metric; }); webVitals.onFID((metric) => { window.__RESULT__.webVitals.fid = metric; });
webVitals.onTTFB((metric) => { window.__RESULT__.webVitals.ttfb = metric; }); webVitals.onTTFB((metric) => { window.__RESULT__.webVitals.ttfb = metric; });
try { try {
${transpiled} ${transpiled}
window.App = App; window.App = App;
// Render the component to the DOM with profiling // Render the component to the DOM with profiling
const AppComponent = window.App || (() => React.createElement('div', null, 'No App component exported')); const AppComponent = window.App || (() => React.createElement('div', null, 'No App component exported'));
const root = ReactDOM.createRoot(document.getElementById('root'), { const root = ReactDOM.createRoot(document.getElementById('root'), {
onUncaughtError: (error, errorInfo) => { onUncaughtError: (error, errorInfo) => {
window.__RESULT__.error = error; window.__RESULT__.error = error;
} }
}); });
const renderStart = performance.now() const renderStart = performance.now()
root.render( root.render(
React.createElement(React.Profiler, { React.createElement(React.Profiler, {
id: 'App', id: 'App',
onRender: (id, phase, actualDuration, baseDuration, startTime, commitTime) => { onRender: (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
window.__RESULT__.reactProfilerMetrics.id = id; window.__RESULT__.reactProfiler.id = id;
window.__RESULT__.reactProfilerMetrics.phase = phase; window.__RESULT__.reactProfiler.phase = phase;
window.__RESULT__.reactProfilerMetrics.actualDuration = actualDuration; window.__RESULT__.reactProfiler.actualDuration = actualDuration;
window.__RESULT__.reactProfilerMetrics.baseDuration = baseDuration; window.__RESULT__.reactProfiler.baseDuration = baseDuration;
window.__RESULT__.reactProfilerMetrics.startTime = startTime; window.__RESULT__.reactProfiler.startTime = startTime;
window.__RESULT__.reactProfilerMetrics.commitTime = commitTime; window.__RESULT__.reactProfiler.commitTime = commitTime;
} }
}, React.createElement(AppComponent)) }, React.createElement(AppComponent))
); );
const renderEnd = performance.now(); const renderEnd = performance.now();
window.__RESULT__.renderTime = renderEnd - renderStart; window.__RESULT__.renderTime = renderEnd - renderStart;
} catch (error) { } catch (error) {
console.error('Error rendering component:', error); console.error('Error rendering component:', error);
window.__RESULT__.error = { window.__RESULT__.error = error;
message: error.message, }
stack: error.stack </script>
}; <script>
} window.onerror = function(message, url, lineNumber) {
</script> const formattedMessage = message + '@' + lineNumber;
<script> if (window.__RESULT__.error && window.__RESULT__.error.message != null) {
window.onerror = function(message, url, lineNumber) { window.__RESULT__.error = window.__RESULT__.error + '\n\n' + formattedMessage;
window.__RESULT__.error = message; } else {
}; window.__RESULT__.error = message + formattedMessage;
</script> }
</body> };
</html> </script>
`; </body>
</html>
`;
return html; return html;
} }