-
Notifications
You must be signed in to change notification settings - Fork 49.9k
[mdn] Initial experiment for adding performance tool #33045
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
7894131
6733b9b
43370f6
609b1f9
c3f2408
8af2d22
29d4c50
1a79060
98d11e2
082829c
b773add
3d64fcb
c6d9e4e
2776fca
6b3e24f
4c9cde2
2d85dea
5350a13
e45b6f8
ed4665c
b6e447c
156bd46
32cccb4
ee7368d
ee05ffc
d1f33b6
6e38fc0
a6a8850
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| module.exports = { | ||
| preset: 'ts-jest', | ||
| testEnvironment: 'node', | ||
| testMatch: ['**/*.test.ts'], | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,50 @@ 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', { | ||
| text: z.string(), | ||
| }, | ||
| async ({text}) => { | ||
| try { | ||
| const performanceResults = await measurePerformance(text); | ||
|
|
||
| const formattedResults = ` | ||
| # React Component Performance Results | ||
|
|
||
| ## Render Time | ||
| ${performanceResults.renderTime.toFixed(2)}ms | ||
|
|
||
| ## Web Vitals | ||
| - Cumulative Layout Shift (CLS): ${performanceResults.webVitals.cls?.value || 'N/A'} | ||
| - Largest Contentful Paint (LCP): ${performanceResults.webVitals.lcp?.value || 'N/A'}ms | ||
| - Interaction to Next Paint (INP): ${performanceResults.webVitals.inp?.value || 'N/A'}ms | ||
| - First Input Delay (FID): ${performanceResults.webVitals.fid?.value || 'N/A'}ms | ||
| - Time to First Byte (TTFB): ${performanceResults.webVitals.ttfb?.value || 'N/A'}ms | ||
|
|
||
| These metrics can help you evaluate the performance of your React component. Lower values generally indicate better performance. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a nice touch. Could you try asking it to run this tool multiple times to see if it improves on these metrics? One more thing we might want to do is to run several iterations of the test in a single tool call to reduce noise. Just using the mean for now seems like a good start. |
||
| `; | ||
|
|
||
| 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); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| import { measurePerformance } from '../utils/runtimePerf'; | ||
| import puppeteer from 'puppeteer'; | ||
|
|
||
| describe('runtimePerf', () => { | ||
| describe('measurePerformance', () => { | ||
| it('should measure a basic React component', async () => { | ||
| const sampleCode = ` | ||
| import React from 'react'; | ||
| function App() { | ||
| return <div>Hello World</div>; | ||
| } | ||
| window.App = App; | ||
| `; | ||
|
|
||
| const result = await measurePerformance(sampleCode); | ||
|
|
||
| expect(result).toHaveProperty('renderTime'); | ||
| expect(result).toHaveProperty('webVitals'); | ||
| console.log(result); | ||
|
||
| }, 300000); | ||
|
|
||
| it('should handle components with state', async () => { | ||
| const complexCode = ` | ||
| import React, { useState } from 'react'; | ||
| export function App() { | ||
| const [count, setCount] = useState(0); | ||
| return ( | ||
| <div> | ||
| <h1>Counter: {count}</h1> | ||
| <button onClick={() => setCount(count + 1)}>Increment</button> | ||
| </div> | ||
| ); | ||
| } | ||
| window.App = App; | ||
| `; | ||
|
|
||
| const result = await measurePerformance(complexCode); | ||
|
|
||
| expect(result).toHaveProperty('renderTime'); | ||
| expect(result).toHaveProperty('webVitals'); | ||
| console.log(result); | ||
| }, 300000); | ||
|
|
||
| it('should measure a component with heavy rendering load', async () => { | ||
|
||
| const heavyRenderCode = ` | ||
| import React, { useState, useMemo } from 'react'; | ||
| // Complex calculation function | ||
| function calculatePrimes(max) { | ||
| const sieve = Array(max).fill(true); | ||
| sieve[0] = sieve[1] = false; | ||
| for (let i = 2; i <= Math.sqrt(max); i++) { | ||
| if (sieve[i]) { | ||
| for (let j = i * i; j < max; j += i) { | ||
| sieve[j] = false; | ||
| } | ||
| } | ||
| } | ||
| return Array.from({ length: max }, (_, i) => i).filter(i => sieve[i]); | ||
| } | ||
| // Recursive Fibonacci - intentionally inefficient | ||
| function fibonacci(n) { | ||
| if (n <= 1) return n; | ||
| return fibonacci(n - 1) + fibonacci(n - 2); | ||
| } | ||
| // Complex nested component | ||
| function ComplexItem({ index, depth = 0 }) { | ||
| // Calculate something expensive for each item | ||
| const fib = React.useMemo(() => fibonacci(depth + 10), [depth]); | ||
| if (depth > 3) { | ||
| return ( | ||
| <div className="item-wrapper" style={{ padding: depth * 5 }}> | ||
| <div className="item"> | ||
| Item {index} (Depth: {depth}, Fibonacci: {fib}) | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| return ( | ||
| <div className="item-wrapper" style={{ padding: depth * 5 }}> | ||
| <div className="item"> | ||
| Item {index} (Depth: {depth}, Fibonacci: {fib}) | ||
| {Array.from({ length: 3 }, (_, i) => ( | ||
| <ComplexItem key={i} index={index + '-' + i} depth={depth + 1} /> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| export function App() { | ||
| const [items, setItems] = React.useState(50); | ||
| // Calculate prime numbers on render - computationally expensive | ||
| const primes = React.useMemo(() => calculatePrimes(1000), []); | ||
| return ( | ||
| <div className="heavy-render-app"> | ||
| <h1>Heavy Rendering Test</h1> | ||
| <p>Found {primes.length} prime numbers</p> | ||
| <div className="controls"> | ||
| <button onClick={() => setItems(prev => Math.max(10, prev - 10))}> | ||
| Fewer Items | ||
| </button> | ||
| <span> {items} items </span> | ||
| <button onClick={() => setItems(prev => prev + 10)}> | ||
| More Items | ||
| </button> | ||
| </div> | ||
| <div className="item-list" style={{ | ||
| display: 'grid', | ||
| gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', | ||
| gap: '10px' | ||
| }}> | ||
| {Array.from({ length: items }, (_, i) => ( | ||
| <ComplexItem key={i} index={i} /> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| window.App = App; | ||
| `; | ||
|
|
||
| const result = await measurePerformance(heavyRenderCode); | ||
|
|
||
| expect(result).toHaveProperty('renderTime'); | ||
| expect(result).toHaveProperty('webVitals'); | ||
| expect(result.renderTime).toBeGreaterThan(10); // Expect rendering to take more than 10ms | ||
| console.log('Heavy render performance:', result); | ||
| }, 600000); // Longer timeout for heavy rendering | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| import * as babel from '@babel/core'; | ||
| import * as parser from '@babel/parser'; | ||
| import puppeteer from 'puppeteer'; | ||
|
|
||
| export async function measurePerformance(code: any) { | ||
| // Parse the code into an AST | ||
| const parsed = await parseAsync(code); | ||
|
|
||
| // Transform the AST into browser-compatible JavaScript | ||
| const transpiled = await transformAsync(parsed, code); | ||
|
|
||
| console.log(transpiled); | ||
|
|
||
| // Launch puppeteer with increased protocol timeout | ||
| const browser = await puppeteer.launch({ | ||
| protocolTimeout: 600_000, // Increase timeout to 10 minutes | ||
| headless: false | ||
|
||
| }); | ||
| 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', {timeout: 600_000}); // 10 minute timeout | ||
|
|
||
| const result = await page.evaluate(() => { | ||
| return (window as any).__RESULT__; | ||
| }); | ||
| // await browser.close(); | ||
| return result; | ||
| } | ||
poteto marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * Parse code string into an AST | ||
| * @param {string} code - The source code to parse | ||
| * @returns {babel.types.File} - The parsed AST | ||
| */ | ||
| function parseAsync(code: string) { | ||
|
||
| return parser.parse(code, { | ||
| sourceType: 'module', | ||
| plugins: [ | ||
| 'jsx', | ||
| 'typescript', | ||
| 'classProperties', | ||
| 'optionalChaining', | ||
| 'nullishCoalescingOperator', | ||
| ], | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * 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, code: string) { | ||
| // Provide a dummy filename to satisfy Babel's requirement for filename context | ||
| const result = await babel.transformFromAstAsync(ast, null, { | ||
| filename: 'file.jsx', | ||
| presets: [ | ||
| ['@babel/preset-env'], | ||
| '@babel/preset-react' | ||
| ], | ||
| plugins: [ | ||
| () => ({ | ||
| visitor: { | ||
| ImportDeclaration(path) { | ||
| const value = path.node.source.value; | ||
| if (value === 'react' || value === 'react-dom') { | ||
| path.remove(); | ||
| } | ||
| } | ||
| } | ||
| }) | ||
| ] | ||
| }); | ||
|
|
||
| return result?.code || ''; | ||
| } | ||
|
|
||
| function buildHtml(transpiled: string | null | undefined) { | ||
|
||
| // Create HTML that includes React, ReactDOM, and the transpiled code | ||
| 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/[email protected]/dist/web-vitals.iife.js"></script> | ||
| <style> | ||
| body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; } | ||
| #root { padding: 20px; } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <div id="root"></div> | ||
| <script type="module"> | ||
| // Store performance metrics | ||
| window.__RESULT__ = { | ||
| renderTime: null, | ||
| webVitals: {} | ||
| }; | ||
| // Measure web vitals | ||
| 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; }); | ||
| // Wrap user code in React.Profiler to measure render performance | ||
| const renderStart = performance.now(); | ||
|
||
| // Execute the transpiled code | ||
| ${transpiled} | ||
| // 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')); | ||
| console.log('rendering...'); | ||
| root.render( | ||
| React.createElement(React.Profiler, { | ||
| id: 'App', | ||
| onRender: (id, phase, actualDuration) => { | ||
| window.__RESULT__.renderTime = actualDuration; | ||
|
||
| } | ||
| }, React.createElement(AppComponent)) | ||
| ); | ||
| </script> | ||
| </body> | ||
| </html> | ||
| `; | ||
|
|
||
| return html; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.