[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', () => ({
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() {
const transport = new StdioServerTransport();
await server.connect(transport);

View File

@ -1,8 +1,32 @@
import * as babel from '@babel/core';
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 = {
filename: 'anonymous.tsx',
configFile: false,
babelrc: false,
presets: [
@ -12,16 +36,13 @@ export async function measurePerformance(code: string) {
],
};
// Parse the code to AST
const parsed = await babel.parseAsync(code, babelOptions);
if (!parsed) {
throw new Error('Failed to parse code');
}
// Transform AST to browser-compatible JavaScript
const transformResult = await babel.transformFromAstAsync(parsed, undefined, {
...babelOptions,
filename: 'file.jsx',
plugins: [
() => ({
visitor: {
@ -44,22 +65,74 @@ export async function measurePerformance(code: string) {
}
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({width: 1280, height: 720});
const html = buildHtml(transpiled);
await page.setContent(html, {waitUntil: 'networkidle0'});
let performanceResults: PerformanceResults = {
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,
};
for (let ii = 0; ii < iterations; ii++) {
await page.setContent(html, {waitUntil: 'networkidle0'});
await page.waitForFunction(
'window.__RESULT__ !== undefined && (window.__RESULT__.renderTime !== null || window.__RESULT__.error !== null)',
);
const result = await page.evaluate(() => {
// 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();
return result;
return performanceResults;
}
function buildHtml(transpiled: string) {
@ -83,7 +156,7 @@ function buildHtml(transpiled: string) {
window.__RESULT__ = {
renderTime: null,
webVitals: {},
reactProfilerMetrics: {},
reactProfiler: {},
error: null
};
@ -113,12 +186,12 @@ function buildHtml(transpiled: string) {
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;
window.__RESULT__.reactProfiler.id = id;
window.__RESULT__.reactProfiler.phase = phase;
window.__RESULT__.reactProfiler.actualDuration = actualDuration;
window.__RESULT__.reactProfiler.baseDuration = baseDuration;
window.__RESULT__.reactProfiler.startTime = startTime;
window.__RESULT__.reactProfiler.commitTime = commitTime;
}
}, React.createElement(AppComponent))
);
@ -128,15 +201,17 @@ function buildHtml(transpiled: string) {
window.__RESULT__.renderTime = renderEnd - renderStart;
} catch (error) {
console.error('Error rendering component:', error);
window.__RESULT__.error = {
message: error.message,
stack: error.stack
};
window.__RESULT__.error = error;
}
</script>
<script>
window.onerror = function(message, url, lineNumber) {
window.__RESULT__.error = message;
const formattedMessage = message + '@' + lineNumber;
if (window.__RESULT__.error && window.__RESULT__.error.message != null) {
window.__RESULT__.error = window.__RESULT__.error + '\n\n' + formattedMessage;
} else {
window.__RESULT__.error = message + formattedMessage;
}
};
</script>
</body>