Monorepo for Aesthetic.Computer aesthetic.computer
at main 246 lines 8.7 kB view raw
1// Chrome DevTools Performance Testing with Puppeteer 2// Automated performance analysis of aesthetic.computer boot sequence 3 4import puppeteer from 'puppeteer'; 5import lighthouse from 'lighthouse'; 6import { writeFileSync } from 'fs'; 7import { join } from 'path'; 8 9const TEST_URL = process.env.TEST_URL || 'https://localhost:8888'; 10const OUTPUT_DIR = './tests/performance/reports'; 11 12/** 13 * Measure boot performance with Chrome Performance API 14 */ 15async function measureBootPerformance() { 16 console.log('🚀 Starting Chrome DevTools Performance Test\n'); 17 18 const browser = await puppeteer.launch({ 19 headless: true, 20 args: [ 21 '--no-sandbox', 22 '--disable-setuid-sandbox', 23 '--ignore-certificate-errors', // For local SSL 24 '--disable-web-security', // For local CORS testing 25 ] 26 }); 27 28 try { 29 const page = await browser.newPage(); 30 31 // Enable Performance monitoring 32 await page.coverage.startJSCoverage(); 33 const client = await page.target().createCDPSession(); 34 await client.send('Performance.enable'); 35 36 console.log('📊 Loading page and capturing metrics...\n'); 37 const startTime = Date.now(); 38 39 // Navigate and wait for network idle 40 await page.goto(TEST_URL, { 41 waitUntil: 'networkidle2', 42 timeout: 30000 43 }); 44 45 // Wait for boot complete or timeout 46 try { 47 await page.waitForFunction( 48 () => window.acBOOT_START_TIME && document.readyState === 'complete', 49 { timeout: 10000 } 50 ); 51 } catch (e) { 52 console.warn('⚠️ Boot completion markers not found, using basic timing'); 53 } 54 55 const loadTime = Date.now() - startTime; 56 57 // Collect Performance metrics 58 const performanceMetrics = await page.evaluate(() => { 59 const perf = performance.getEntriesByType('navigation')[0]; 60 const paint = performance.getEntriesByType('paint'); 61 62 return { 63 // Navigation timing 64 dns: perf.domainLookupEnd - perf.domainLookupStart, 65 tcp: perf.connectEnd - perf.connectStart, 66 ttfb: perf.responseStart - perf.requestStart, 67 download: perf.responseEnd - perf.responseStart, 68 domInteractive: perf.domInteractive, 69 domComplete: perf.domComplete, 70 loadComplete: perf.loadEventEnd - perf.loadEventStart, 71 72 // Paint timing 73 firstPaint: paint.find(p => p.name === 'first-paint')?.startTime || 0, 74 firstContentfulPaint: paint.find(p => p.name === 'first-contentful-paint')?.startTime || 0, 75 76 // Custom timing 77 bootStartTime: window.acBOOT_START_TIME, 78 currentTime: performance.now(), 79 }; 80 }); 81 82 // Get resource timing 83 const resources = await page.evaluate(() => { 84 return performance.getEntriesByType('resource').map(r => ({ 85 name: r.name.split('/').pop() || r.name, 86 duration: r.duration, 87 size: r.transferSize, 88 type: r.initiatorType, 89 })).sort((a, b) => b.duration - a.duration).slice(0, 10); // Top 10 slowest 90 }); 91 92 // Get JS coverage 93 const jsCoverage = await page.coverage.stopJSCoverage(); 94 const totalBytes = jsCoverage.reduce((sum, entry) => sum + entry.text.length, 0); 95 const usedBytes = jsCoverage.reduce((sum, entry) => { 96 const used = entry.ranges.reduce((s, range) => s + (range.end - range.start), 0); 97 return sum + used; 98 }, 0); 99 const coveragePercent = ((usedBytes / totalBytes) * 100).toFixed(1); 100 101 // Print results 102 console.log('⏱️ Performance Metrics:'); 103 console.log(` Total Load Time: ${loadTime}ms`); 104 console.log(` DNS Lookup: ${performanceMetrics.dns.toFixed(2)}ms`); 105 console.log(` TCP Connect: ${performanceMetrics.tcp.toFixed(2)}ms`); 106 console.log(` Time to First Byte: ${performanceMetrics.ttfb.toFixed(2)}ms`); 107 console.log(` Download: ${performanceMetrics.download.toFixed(2)}ms`); 108 console.log(` DOM Interactive: ${performanceMetrics.domInteractive.toFixed(2)}ms`); 109 console.log(` DOM Complete: ${performanceMetrics.domComplete.toFixed(2)}ms`); 110 console.log(` First Paint: ${performanceMetrics.firstPaint.toFixed(2)}ms`); 111 console.log(` First Contentful Paint: ${performanceMetrics.firstContentfulPaint.toFixed(2)}ms\n`); 112 113 console.log('📦 Top 10 Slowest Resources:'); 114 resources.forEach((r, i) => { 115 const size = r.size ? `(${(r.size / 1024).toFixed(1)}KB)` : ''; 116 console.log(` ${i + 1}. ${r.name} - ${r.duration.toFixed(2)}ms ${size}`); 117 }); 118 console.log(); 119 120 console.log('📊 JavaScript Coverage:'); 121 console.log(` Total JS: ${(totalBytes / 1024).toFixed(1)}KB`); 122 console.log(` Used JS: ${(usedBytes / 1024).toFixed(1)}KB`); 123 console.log(` Coverage: ${coveragePercent}%`); 124 console.log(` Unused: ${((totalBytes - usedBytes) / 1024).toFixed(1)}KB\n`); 125 126 // Performance thresholds 127 const thresholds = { 128 fcp: 1500, // First Contentful Paint 129 tti: 3000, // Time to Interactive (approximated by domComplete) 130 load: 5000, // Total load time 131 }; 132 133 console.log('✅ Performance Checks:'); 134 console.log(` ${performanceMetrics.firstContentfulPaint < thresholds.fcp ? '✅' : '❌'} FCP under ${thresholds.fcp}ms: ${performanceMetrics.firstContentfulPaint.toFixed(0)}ms`); 135 console.log(` ${performanceMetrics.domComplete < thresholds.tti ? '✅' : '❌'} TTI under ${thresholds.tti}ms: ${performanceMetrics.domComplete.toFixed(0)}ms`); 136 console.log(` ${loadTime < thresholds.load ? '✅' : '❌'} Load under ${thresholds.load}ms: ${loadTime}ms\n`); 137 138 return { 139 loadTime, 140 metrics: performanceMetrics, 141 resources, 142 coverage: { totalBytes, usedBytes, percent: coveragePercent }, 143 }; 144 145 } finally { 146 await browser.close(); 147 } 148} 149 150/** 151 * Run Lighthouse audit 152 */ 153async function runLighthouseAudit() { 154 console.log('💡 Running Lighthouse Audit...\n'); 155 156 const browser = await puppeteer.launch({ 157 headless: true, 158 args: [ 159 '--no-sandbox', 160 '--disable-setuid-sandbox', 161 '--ignore-certificate-errors', 162 ] 163 }); 164 165 try { 166 const { lhr } = await lighthouse(TEST_URL, { 167 port: new URL(browser.wsEndpoint()).port, 168 output: 'json', 169 onlyCategories: ['performance'], 170 formFactor: 'desktop', 171 screenEmulation: { disabled: true }, 172 }); 173 174 const scores = { 175 performance: lhr.categories.performance.score * 100, 176 fcp: lhr.audits['first-contentful-paint'].numericValue, 177 lcp: lhr.audits['largest-contentful-paint'].numericValue, 178 tbt: lhr.audits['total-blocking-time'].numericValue, 179 cls: lhr.audits['cumulative-layout-shift'].numericValue, 180 speedIndex: lhr.audits['speed-index'].numericValue, 181 }; 182 183 console.log('💡 Lighthouse Performance Score:'); 184 console.log(` Overall: ${scores.performance.toFixed(0)}/100`); 185 console.log(` First Contentful Paint: ${scores.fcp.toFixed(0)}ms`); 186 console.log(` Largest Contentful Paint: ${scores.lcp.toFixed(0)}ms`); 187 console.log(` Total Blocking Time: ${scores.tbt.toFixed(0)}ms`); 188 console.log(` Cumulative Layout Shift: ${scores.cls.toFixed(3)}`); 189 console.log(` Speed Index: ${scores.speedIndex.toFixed(0)}ms\n`); 190 191 // Save full report 192 const reportPath = join(OUTPUT_DIR, `lighthouse-${Date.now()}.json`); 193 try { 194 writeFileSync(reportPath, JSON.stringify(lhr, null, 2)); 195 console.log(`📄 Full report saved to: ${reportPath}\n`); 196 } catch (e) { 197 console.log('⚠️ Could not save report (directory may not exist)\n'); 198 } 199 200 return scores; 201 202 } finally { 203 await browser.close(); 204 } 205} 206 207/** 208 * Main test runner 209 */ 210async function runTests() { 211 try { 212 console.log('🎯 Chrome DevTools Performance Testing\n'); 213 console.log(`Testing URL: ${TEST_URL}\n`); 214 console.log('─'.repeat(60) + '\n'); 215 216 // Run performance measurement 217 const perfResults = await measureBootPerformance(); 218 219 console.log('─'.repeat(60) + '\n'); 220 221 // Run Lighthouse audit (optional, can be slow) 222 if (process.env.RUN_LIGHTHOUSE !== 'false') { 223 const lighthouseResults = await runLighthouseAudit(); 224 console.log('─'.repeat(60) + '\n'); 225 } 226 227 console.log('✅ All tests completed!\n'); 228 229 // Exit with error if performance is poor 230 if (perfResults.loadTime > 5000) { 231 console.error('❌ Performance threshold exceeded!'); 232 process.exit(1); 233 } 234 235 } catch (error) { 236 console.error('❌ Test failed:', error.message); 237 process.exit(1); 238 } 239} 240 241// Run tests if called directly 242if (import.meta.url === `file://${process.argv[1]}`) { 243 runTests(); 244} 245 246export { measureBootPerformance, runLighthouseAudit };