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