@samitouri / QOS-React / commits / dc2b11817b

[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.

lauren committed May 2, 2025 at 14:15 UTC dc2b11817bbfcd39f1dbdc8945acbf18cb5e41c3
2 files changed +243 -214
compiler/packages/react-mcp-server/src/index.ts
+77 -123
@@ -275,6 +275,83 @@ server.tool(
275 },
276 );
277
278 +server.tool(
279 + 'review-react-runtime',
280 + `Run this tool every time you propose a performance related change to verify if your suggestion actually improves performance.
281 + <requirements>
282 + This tool has some requirements on the code input:
283 + - The react code that is passed into this tool MUST contain an App functional component without arrow function.
284 + - DO NOT export anything since we can't parse export syntax with this tool.
285 + - Only import React from 'react' and use all hooks and imports using the React. prefix like React.useState and React.useEffect
286 + </requirements>
287 +
288 + <goals>
289 + - LCP - loading speed: good ≤ 2.5 s, needs-improvement 2.5-4 s, poor > 4 s
290 + - INP - input responsiveness: good ≤ 200 ms, needs-improvement 200-500 ms, poor > 500 ms
291 + - CLS - visual stability: good ≤ 0.10, needs-improvement 0.10-0.25, poor > 0.25
292 + - (Optional: FCP ≤ 1.8 s, TTFB ≤ 0.8 s)
293 + </goals>
294 +
295 + <evaluation>
296 + Classify each metric with the thresholds above. Identify the worst category in the order poor > needs-improvement > good.
297 + </evaluation>
298 +
299 + <iterate>
300 + (repeat until every metric is good or two consecutive cycles show no gain)
301 + - Apply one focused change based on the failing metric plus React-specific guidance:
302 + - 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).
303 + - INP: wrap non-critical updates in useTransition, avoid calling setState inside useEffect.
304 + - 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.
305 +
306 + Stop when every metric is classified as good. Return the final metric table and the list of applied changes.
307 + </iterate>
308 + `,
309 + {
310 + text: z.string(),
311 + iterations: z.number().optional().default(2),
312 + },
313 + async ({text, iterations}) => {
314 + try {
315 + const results = await measurePerformance(text, iterations);
316 + const formattedResults = `
317 +# React Component Performance Results
318 +
319 +## Mean Render Time
320 +${results.renderTime / iterations}ms
321 +
322 +## Mean Web Vitals
323 +- Cumulative Layout Shift (CLS): ${results.webVitals.cls / iterations}ms
324 +- Largest Contentful Paint (LCP): ${results.webVitals.lcp / iterations}ms
325 +- Interaction to Next Paint (INP): ${results.webVitals.inp / iterations}ms
326 +- First Input Delay (FID): ${results.webVitals.fid / iterations}ms
327 +
328 +## Mean React Profiler
329 +- Actual Duration: ${results.reactProfiler.actualDuration / iterations}ms
330 +- Base Duration: ${results.reactProfiler.baseDuration / iterations}ms
331 +`;
332 +
333 + return {
334 + content: [
335 + {
336 + type: 'text' as const,
337 + text: formattedResults,
338 + },
339 + ],
340 + };
341 + } catch (error) {
342 + return {
343 + isError: true,
344 + content: [
345 + {
346 + type: 'text' as const,
347 + text: `Error measuring performance: ${error.message}\n\n${error.stack}`,
348 + },
349 + ],
350 + };
351 + }
352 + },
353 +);
354 +
355 server.prompt('review-react-code', () => ({
356 messages: [
357 {
@@ -354,129 +431,6 @@ Server Components - Shift data-heavy logic to the server whenever possible. Brea
431 ],
432 }));
433
357 -server.tool(
358 - 'review-react-runtime',
359 - `Run this tool every time you propose a performance related change to verify if your suggestion actually improves performance.
360 - <requirements>
361 - This tool has some requirements on the code input:
362 - - The react code that is passed into this tool MUST contain an App functional component without arrow function.
363 - - DO NOT export anything since we can't parse export syntax with this tool.
364 - - Only import React from 'react' and use all hooks and imports using the React. prefix like React.useState and React.useEffect
365 - </requirements>
366 -
367 - <goals>
368 - - LCP - loading speed: good ≤ 2.5 s, needs-improvement 2.5-4 s, poor > 4 s
369 - - INP - input responsiveness: good ≤ 200 ms, needs-improvement 200-500 ms, poor > 500 ms
370 - - CLS - visual stability: good ≤ 0.10, needs-improvement 0.10-0.25, poor > 0.25
371 - - (Optional: FCP ≤ 1.8 s, TTFB ≤ 0.8 s)
372 - </goals>
373 -
374 - <evaluation>
375 - Classify each metric with the thresholds above. Identify the worst category in the order poor > needs-improvement > good.
376 - </evaluation>
377 -
378 - <iterate>
379 - (repeat until every metric is good or two consecutive cycles show no gain)
380 - - Apply one focused change based on the failing metric plus React-specific guidance:
381 - - 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).
382 - - INP: wrap non-critical updates in useTransition, avoid calling setState inside useEffect.
383 - - 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.
384 -
385 - Stop when every metric is classified as good. Return the final metric table and the list of applied changes.
386 - </iterate>
387 - `,
388 - {
389 - text: z.string(),
390 - },
391 - async ({text}) => {
392 - try {
393 - const iterations = 20;
394 -
395 - let perfData = {
396 - renderTime: 0,
397 - webVitals: {
398 - cls: 0,
399 - lcp: 0,
400 - inp: 0,
401 - fid: 0,
402 - ttfb: 0,
403 - },
404 - reactProfilerMetrics: {
405 - id: 0,
406 - phase: 0,
407 - actualDuration: 0,
408 - baseDuration: 0,
409 - startTime: 0,
410 - commitTime: 0,
411 - },
412 - error: null,
413 - };
414 -
415 - for (let i = 0; i < iterations; i++) {
416 - const performanceResults = await measurePerformance(text);
417 - perfData.renderTime += performanceResults.renderTime;
418 - perfData.webVitals.cls += performanceResults.webVitals.cls || 0;
419 - perfData.webVitals.lcp += performanceResults.webVitals.lcp || 0;
420 - perfData.webVitals.inp += performanceResults.webVitals.inp || 0;
421 - perfData.webVitals.fid += performanceResults.webVitals.fid || 0;
422 - perfData.webVitals.ttfb += performanceResults.webVitals.ttfb || 0;
423 -
424 - perfData.reactProfilerMetrics.id +=
425 - performanceResults.reactProfilerMetrics.actualDuration || 0;
426 - perfData.reactProfilerMetrics.phase +=
427 - performanceResults.reactProfilerMetrics.phase || 0;
428 - perfData.reactProfilerMetrics.actualDuration +=
429 - performanceResults.reactProfilerMetrics.actualDuration || 0;
430 - perfData.reactProfilerMetrics.baseDuration +=
431 - performanceResults.reactProfilerMetrics.baseDuration || 0;
432 - perfData.reactProfilerMetrics.startTime +=
433 - performanceResults.reactProfilerMetrics.startTime || 0;
434 - perfData.reactProfilerMetrics.commitTime +=
435 - performanceResults.reactProfilerMetrics.commitTime || 0;
436 - }
437 -
438 - const formattedResults = `
439 -# React Component Performance Results
440 -
441 -## Mean Render Time
442 -${perfData.renderTime / iterations}ms
443 -
444 -## Mean Web Vitals
445 -- Cumulative Layout Shift (CLS): ${perfData.webVitals.cls / iterations}
446 -- Largest Contentful Paint (LCP): ${perfData.webVitals.lcp / iterations}ms
447 -- Interaction to Next Paint (INP): ${perfData.webVitals.inp / iterations}ms
448 -- First Input Delay (FID): ${perfData.webVitals.fid / iterations}ms
449 -- Time to First Byte (TTFB): ${perfData.webVitals.ttfb / iterations}ms
450 -
451 -## Mean React Profiler
452 -- Actual Duration: ${perfData.reactProfilerMetrics.actualDuration / iterations}ms
453 -- Base Duration: ${perfData.reactProfilerMetrics.baseDuration / iterations}ms
454 -- Start Time: ${perfData.reactProfilerMetrics.startTime / iterations}ms
455 -- Commit Time: ${perfData.reactProfilerMetrics.commitTime / iterations}ms
456 -`;
457 -
458 - return {
459 - content: [
460 - {
461 - type: 'text' as const,
462 - text: formattedResults,
463 - },
464 - ],
465 - };
466 - } catch (error) {
467 - return {
468 - isError: true,
469 - content: [
470 - {
471 - type: 'text' as const,
472 - text: `Error measuring performance: ${error.message}\n\n${error.stack}`,
473 - },
474 - ],
475 - };
476 - }
477 - },
478 -);
479 -
434 async function main() {
435 const transport = new StdioServerTransport();
436 await server.connect(transport);
compiler/packages/react-mcp-server/src/tools/runtimePerf.ts
+166 -91
@@ -1,8 +1,32 @@
1 import * as babel from '@babel/core';
2 import puppeteer from 'puppeteer';
3
4 -export async function measurePerformance(code: string) {
4 +type PerformanceResults = {
5 + renderTime: number;
6 + webVitals: {
7 + cls: number;
8 + lcp: number;
9 + inp: number;
10 + fid: number;
11 + ttfb: number;
12 + };
13 + reactProfiler: {
14 + id: number;
15 + phase: number;
16 + actualDuration: number;
17 + baseDuration: number;
18 + startTime: number;
19 + commitTime: number;
20 + };
21 + error: Error | null;
22 +};
23 +
24 +export async function measurePerformance(
25 + code: string,
26 + iterations: number,
27 +): Promise<PerformanceResults> {
28 const babelOptions = {
29 + filename: 'anonymous.tsx',
30 configFile: false,
31 babelrc: false,
32 presets: [
@@ -12,16 +36,13 @@ export async function measurePerformance(code: string) {
36 ],
37 };
38
15 - // Parse the code to AST
39 const parsed = await babel.parseAsync(code, babelOptions);
40 if (!parsed) {
41 throw new Error('Failed to parse code');
42 }
43
21 - // Transform AST to browser-compatible JavaScript
44 const transformResult = await babel.transformFromAstAsync(parsed, undefined, {
45 ...babelOptions,
24 - filename: 'file.jsx',
46 plugins: [
47 () => ({
48 visitor: {
@@ -44,104 +65,158 @@ export async function measurePerformance(code: string) {
65 }
66
67 const browser = await puppeteer.launch();
47 -
68 const page = await browser.newPage();
69 await page.setViewport({width: 1280, height: 720});
70 const html = buildHtml(transpiled);
51 - await page.setContent(html, {waitUntil: 'networkidle0'});
71
53 - await page.waitForFunction(
54 - 'window.__RESULT__ !== undefined && (window.__RESULT__.renderTime !== null || window.__RESULT__.error !== null)',
55 - );
72 + let performanceResults: PerformanceResults = {
73 + renderTime: 0,
74 + webVitals: {
75 + cls: 0,
76 + lcp: 0,
77 + inp: 0,
78 + fid: 0,
79 + ttfb: 0,
80 + },
81 + reactProfiler: {
82 + id: 0,
83 + phase: 0,
84 + actualDuration: 0,
85 + baseDuration: 0,
86 + startTime: 0,
87 + commitTime: 0,
88 + },
89 + error: null,
90 + };
91
57 - const result = await page.evaluate(() => {
58 - return (window as any).__RESULT__;
59 - });
92 + for (let ii = 0; ii < iterations; ii++) {
93 + await page.setContent(html, {waitUntil: 'networkidle0'});
94 + await page.waitForFunction(
95 + 'window.__RESULT__ !== undefined && (window.__RESULT__.renderTime !== null || window.__RESULT__.error !== null)',
96 + );
97 + // ui chaos monkey
98 + await page.waitForFunction(`window.__RESULT__ !== undefined && (function() {
99 + for (const el of [...document.querySelectorAll('a'), ...document.querySelectorAll('button')]) {
100 + console.log(el);
101 + el.click();
102 + }
103 + return true;
104 + })() `);
105 + const evaluationResult: PerformanceResults = await page.evaluate(() => {
106 + return (window as any).__RESULT__;
107 + });
108 +
109 + // TODO: investigate why webvital metrics are not populating correctly
110 + performanceResults.renderTime += evaluationResult.renderTime;
111 + performanceResults.webVitals.cls += evaluationResult.webVitals.cls || 0;
112 + performanceResults.webVitals.lcp += evaluationResult.webVitals.lcp || 0;
113 + performanceResults.webVitals.inp += evaluationResult.webVitals.inp || 0;
114 + performanceResults.webVitals.fid += evaluationResult.webVitals.fid || 0;
115 + performanceResults.webVitals.ttfb += evaluationResult.webVitals.ttfb || 0;
116 +
117 + performanceResults.reactProfiler.id +=
118 + evaluationResult.reactProfiler.actualDuration || 0;
119 + performanceResults.reactProfiler.phase +=
120 + evaluationResult.reactProfiler.phase || 0;
121 + performanceResults.reactProfiler.actualDuration +=
122 + evaluationResult.reactProfiler.actualDuration || 0;
123 + performanceResults.reactProfiler.baseDuration +=
124 + evaluationResult.reactProfiler.baseDuration || 0;
125 + performanceResults.reactProfiler.startTime +=
126 + evaluationResult.reactProfiler.startTime || 0;
127 + performanceResults.reactProfiler.commitTime +=
128 + evaluationResult.reactProfiler.commitTime || 0;
129 +
130 + performanceResults.error = evaluationResult.error;
131 + }
132
133 await browser.close();
62 - return result;
134 +
135 + return performanceResults;
136 }
137
138 function buildHtml(transpiled: string) {
139 const html = `
67 - <!DOCTYPE html>
68 - <html>
69 - <head>
70 - <meta charset="UTF-8">
71 - <title>React Performance Test</title>
72 - <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
73 - <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
74 - <script src="https://unpkg.com/web-vitals@3.0.0/dist/web-vitals.iife.js"></script>
75 - <style>
76 - body { margin: 0; }
77 - #root { padding: 20px; }
78 - </style>
79 - </head>
80 - <body>
81 - <div id="root"></div>
82 - <script>
83 - window.__RESULT__ = {
84 - renderTime: null,
85 - webVitals: {},
86 - reactProfilerMetrics: {},
87 - error: null
88 - };
89 -
90 - webVitals.onCLS((metric) => { window.__RESULT__.webVitals.cls = metric; });
91 - webVitals.onLCP((metric) => { window.__RESULT__.webVitals.lcp = metric; });
92 - webVitals.onINP((metric) => { window.__RESULT__.webVitals.inp = metric; });
93 - webVitals.onFID((metric) => { window.__RESULT__.webVitals.fid = metric; });
94 - webVitals.onTTFB((metric) => { window.__RESULT__.webVitals.ttfb = metric; });
95 -
96 - try {
97 - ${transpiled}
98 -
99 - window.App = App;
100 -
101 - // Render the component to the DOM with profiling
102 - const AppComponent = window.App || (() => React.createElement('div', null, 'No App component exported'));
103 -
104 - const root = ReactDOM.createRoot(document.getElementById('root'), {
105 - onUncaughtError: (error, errorInfo) => {
106 - window.__RESULT__.error = error;
107 - }
108 - });
109 -
110 - const renderStart = performance.now()
111 -
112 - root.render(
113 - React.createElement(React.Profiler, {
114 - id: 'App',
115 - onRender: (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
116 - window.__RESULT__.reactProfilerMetrics.id = id;
117 - window.__RESULT__.reactProfilerMetrics.phase = phase;
118 - window.__RESULT__.reactProfilerMetrics.actualDuration = actualDuration;
119 - window.__RESULT__.reactProfilerMetrics.baseDuration = baseDuration;
120 - window.__RESULT__.reactProfilerMetrics.startTime = startTime;
121 - window.__RESULT__.reactProfilerMetrics.commitTime = commitTime;
122 - }
123 - }, React.createElement(AppComponent))
124 - );
125 -
126 - const renderEnd = performance.now();
127 -
128 - window.__RESULT__.renderTime = renderEnd - renderStart;
129 - } catch (error) {
130 - console.error('Error rendering component:', error);
131 - window.__RESULT__.error = {
132 - message: error.message,
133 - stack: error.stack
134 - };
135 - }
136 - </script>
137 - <script>
138 - window.onerror = function(message, url, lineNumber) {
139 - window.__RESULT__.error = message;
140 - };
141 - </script>
142 - </body>
143 - </html>
144 - `;
140 +<!DOCTYPE html>
141 +<html>
142 +<head>
143 + <meta charset="UTF-8">
144 + <title>React Performance Test</title>
145 + <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
146 + <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
147 + <script src="https://unpkg.com/web-vitals@3.0.0/dist/web-vitals.iife.js"></script>
148 + <style>
149 + body { margin: 0; }
150 + #root { padding: 20px; }
151 + </style>
152 +</head>
153 +<body>
154 + <div id="root"></div>
155 + <script>
156 + window.__RESULT__ = {
157 + renderTime: null,
158 + webVitals: {},
159 + reactProfiler: {},
160 + error: null
161 + };
162 +
163 + webVitals.onCLS((metric) => { window.__RESULT__.webVitals.cls = metric; });
164 + webVitals.onLCP((metric) => { window.__RESULT__.webVitals.lcp = metric; });
165 + webVitals.onINP((metric) => { window.__RESULT__.webVitals.inp = metric; });
166 + webVitals.onFID((metric) => { window.__RESULT__.webVitals.fid = metric; });
167 + webVitals.onTTFB((metric) => { window.__RESULT__.webVitals.ttfb = metric; });
168 +
169 + try {
170 + ${transpiled}
171 +
172 + window.App = App;
173 +
174 + // Render the component to the DOM with profiling
175 + const AppComponent = window.App || (() => React.createElement('div', null, 'No App component exported'));
176 +
177 + const root = ReactDOM.createRoot(document.getElementById('root'), {
178 + onUncaughtError: (error, errorInfo) => {
179 + window.__RESULT__.error = error;
180 + }
181 + });
182 +
183 + const renderStart = performance.now()
184 +
185 + root.render(
186 + React.createElement(React.Profiler, {
187 + id: 'App',
188 + onRender: (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
189 + window.__RESULT__.reactProfiler.id = id;
190 + window.__RESULT__.reactProfiler.phase = phase;
191 + window.__RESULT__.reactProfiler.actualDuration = actualDuration;
192 + window.__RESULT__.reactProfiler.baseDuration = baseDuration;
193 + window.__RESULT__.reactProfiler.startTime = startTime;
194 + window.__RESULT__.reactProfiler.commitTime = commitTime;
195 + }
196 + }, React.createElement(AppComponent))
197 + );
198 +
199 + const renderEnd = performance.now();
200 +
201 + window.__RESULT__.renderTime = renderEnd - renderStart;
202 + } catch (error) {
203 + console.error('Error rendering component:', error);
204 + window.__RESULT__.error = error;
205 + }
206 + </script>
207 + <script>
208 + window.onerror = function(message, url, lineNumber) {
209 + const formattedMessage = message + '@' + lineNumber;
210 + if (window.__RESULT__.error && window.__RESULT__.error.message != null) {
211 + window.__RESULT__.error = window.__RESULT__.error + '\n\n' + formattedMessage;
212 + } else {
213 + window.__RESULT__.error = message + formattedMessage;
214 + }
215 + };
216 + </script>
217 +</body>
218 +</html>
219 +`;
220
221 return html;
222 }