/** * Dynamic performance analysis using Playwright. * Measures: page load, bundle sizes, memory, rendering, network. */ import { chromium } from 'playwright'; import { readdirSync, statSync } from 'fs'; import { join } from 'path'; const DIST = join(import.meta.dirname, '..', 'dist'); const PORT = 4174; // ─── Bundle analysis from dist ─── function analyzeBundles() { const assets = readdirSync(join(DIST, 'assets')); const jsFiles = assets.filter(f => f.endsWith('.js')); const cssFiles = assets.filter(f => f.endsWith('.css')); const brFiles = assets.filter(f => f.endsWith('.js.br')); let totalJsSize = 0, totalCssSize = 0, totalBrSize = 0; const bundles = []; for (const f of jsFiles) { const size = statSync(join(DIST, 'assets', f)).size; totalJsSize += size; bundles.push({ name: f, sizeKB: (size / 1024).toFixed(2), type: 'js' }); } for (const f of cssFiles) { const size = statSync(join(DIST, 'assets', f)).size; totalCssSize += size; bundles.push({ name: f, sizeKB: (size / 1024).toFixed(2), type: 'css' }); } for (const f of brFiles) { const size = statSync(join(DIST, 'assets', f)).size; totalBrSize += size; } bundles.sort((a, b) => parseFloat(b.sizeKB) - parseFloat(a.sizeKB)); console.log('\n═══════════════════════════════════════════════'); console.log(' BUNDLE SIZE ANALYSIS'); console.log('═══════════════════════════════════════════════'); console.log(`\nTotal JS (raw): ${(totalJsSize / 1024).toFixed(1)} KB`); console.log(`Total CSS (raw): ${(totalCssSize / 1024).toFixed(1)} KB`); console.log(`Total JS (brotli): ${(totalBrSize / 1024).toFixed(1)} KB`); console.log(`\nTop 15 bundles by raw size:`); for (const b of bundles.slice(0, 15)) { const bar = '█'.repeat(Math.min(40, Math.round(parseFloat(b.sizeKB) / 5))); console.log(` ${b.name.padEnd(45)} ${String(b.sizeKB).padStart(8)} KB ${bar}`); } // Identify oversized chunks console.log('\n⚠️ Oversized chunks (>50KB raw):'); for (const b of bundles.filter(b => parseFloat(b.sizeKB) > 50)) { console.log(` [WARN] ${b.name} = ${b.sizeKB} KB`); } return { totalJsSize, totalCssSize, totalBrSize, bundles }; } // ─── Runtime performance with Playwright ─── async function runtimeAnalysis() { console.log('\n═══════════════════════════════════════════════'); console.log(' RUNTIME PERFORMANCE ANALYSIS'); console.log('═══════════════════════════════════════════════'); const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ viewport: { width: 1920, height: 1080 } }); const page = await context.newPage(); // Collect performance metrics const perfEntries = []; page.on('console', msg => { if (msg.type() === 'info' && msg.text().startsWith('[PERF]')) { perfEntries.push(msg.text()); } }); // Track network requests const networkRequests = []; page.on('request', req => { networkRequests.push({ url: req.url(), method: req.method(), startTime: Date.now() }); }); const networkResponses = []; page.on('response', res => { networkResponses.push({ url: res.url(), status: res.status(), size: res.headers()['content-length'] || '0', endTime: Date.now() }); }); // Measure page load console.log('\n--- Page Load Performance ---'); const navStart = Date.now(); try { const response = await page.goto(`http://127.0.0.1:${PORT}/`, { waitUntil: 'networkidle', timeout: 30000 }); const loadTime = Date.now() - navStart; console.log(` Initial page load: ${loadTime}ms`); console.log(` HTTP status: ${response.status()}`); // Get navigation timing from the browser const navTiming = await page.evaluate(() => { const perf = performance.getEntriesByType('navigation')[0]; if (!perf) return null; return { dns: Math.round(perf.domainLookupEnd - perf.domainLookupStart), tcp: Math.round(perf.connectEnd - perf.connectStart), ttfb: Math.round(perf.responseStart - perf.requestStart), download: Math.round(perf.responseEnd - perf.responseStart), domInteractive: Math.round(perf.domInteractive), domComplete: Math.round(perf.domComplete), loadEvent: Math.round(perf.loadEventEnd), transferSize: perf.transferSize, }; }); if (navTiming) { console.log(`\n Navigation Timing:`); console.log(` DNS lookup: ${navTiming.dns}ms`); console.log(` TCP connect: ${navTiming.tcp}ms`); console.log(` TTFB: ${navTiming.ttfb}ms`); console.log(` Download: ${navTiming.download}ms`); console.log(` DOM interactive: ${navTiming.domInteractive}ms`); console.log(` DOM complete: ${navTiming.domComplete}ms`); console.log(` Load event end: ${navTiming.loadEvent}ms`); console.log(` Transfer size: ${(navTiming.transferSize / 1024).toFixed(1)} KB`); } // Resource timing const resources = await page.evaluate(() => { return performance.getEntriesByType('resource').map(r => ({ name: r.name.split('/').pop(), type: r.initiatorType, duration: Math.round(r.duration), size: r.transferSize, })); }); console.log(`\n--- Resource Loading ---`); console.log(` Total resources: ${resources.length}`); const totalTransfer = resources.reduce((s, r) => s + r.size, 0); console.log(` Total transfer: ${(totalTransfer / 1024).toFixed(1)} KB`); const slowResources = resources.filter(r => r.duration > 100).sort((a, b) => b.duration - a.duration); if (slowResources.length > 0) { console.log(`\n Slow resources (>100ms):`); for (const r of slowResources.slice(0, 10)) { console.log(` [SLOW] ${r.name.padEnd(40)} ${r.duration}ms (${(r.size/1024).toFixed(1)}KB)`); } } // Memory analysis console.log(`\n--- Memory Analysis ---`); const memory = await page.evaluate(() => { if (performance.memory) { return { usedJSHeap: performance.memory.usedJSHeapSize, totalJSHeap: performance.memory.totalJSHeapSize, heapLimit: performance.memory.jsHeapSizeLimit, }; } return null; }); if (memory) { console.log(` Used JS heap: ${(memory.usedJSHeap / 1024 / 1024).toFixed(1)} MB`); console.log(` Total JS heap: ${(memory.totalJSHeap / 1024 / 1024).toFixed(1)} MB`); console.log(` Heap limit: ${(memory.heapLimit / 1024 / 1024).toFixed(1)} MB`); console.log(` Heap utilization: ${((memory.usedJSHeap / memory.heapLimit) * 100).toFixed(1)}%`); } else { console.log(' Memory API not available (Chromium flag needed: --enable-precise-memory-info)'); } // DOM complexity console.log(`\n--- DOM Complexity ---`); const domStats = await page.evaluate(() => { const allElements = document.querySelectorAll('*'); const tagCounts = {}; let maxDepth = 0; const totalNodes = allElements.length; allElements.forEach(el => { const tag = el.tagName.toLowerCase(); tagCounts[tag] = (tagCounts[tag] || 0) + 1; let depth = 0, parent = el.parentElement; while (parent) { depth++; parent = parent.parentElement; } if (depth > maxDepth) maxDepth = depth; }); return { totalNodes, maxDepth, tagCounts }; }); console.log(` Total DOM nodes: ${domStats.totalNodes}`); console.log(` Max DOM depth: ${domStats.maxDepth}`); console.log(` Top 10 tags:`); const sortedTags = Object.entries(domStats.tagCounts).sort((a, b) => b[1] - a[1]); for (const [tag, count] of sortedTags.slice(0, 10)) { console.log(` <${tag}>: ${count}`); } // DOM warnings if (domStats.totalNodes > 1500) { console.log(` ⚠️ DOM nodes > 1500 — may cause sluggish rendering`); } if (domStats.maxDepth > 15) { console.log(` ⚠️ DOM depth > 15 — may slow style/layout calculations`); } // React-specific analysis: check for unnecessary re-renders console.log(`\n--- Render Performance ---`); const renderMetrics = await page.evaluate(() => { // Check if React DevTools hook exists const hasReact = !!window.__REACT_DEVTOOLS_GLOBAL_HOOK__; return { hasReact, eventListeners: typeof getEventListeners !== 'undefined' ? 'available' : 'not-in-page-context', }; }); console.log(` React DevTools: ${renderMetrics.hasReact ? 'detected' : 'not detected'}`); // Measure interaction performance - simulate scroll console.log(`\n--- Interaction Performance ---`); const scrollStart = Date.now(); await page.evaluate(() => { const container = document.querySelector('.app-shell-content') || document.documentElement; for (let i = 0; i < 10; i++) { container.scrollTop = i * 100; } container.scrollTop = 0; }); const scrollTime = Date.now() - scrollStart; console.log(` 10x scroll ops: ${scrollTime}ms`); // Navigate to different routes to test lazy loading const routes = ['#/', '#/canvas', '#/workbench', '#/ecommerce', '#/image-workbench', '#/home']; console.log(`\n--- Route Navigation (Lazy Loading) ---`); for (const route of routes) { const routeStart = Date.now(); await page.goto(`http://127.0.0.1:${PORT}/${route}`, { waitUntil: 'domcontentloaded', timeout: 15000 }); const routeTime = Date.now() - routeStart; console.log(` ${route.padEnd(30)} ${routeTime}ms`); } // Memory after navigation const memoryAfter = await page.evaluate(() => { if (performance.memory) { return { usedJSHeap: performance.memory.usedJSHeapSize, totalJSHeap: performance.memory.totalJSHeapSize, }; } return null; }); if (memoryAfter) { console.log(`\n--- Memory After Route Navigation ---`); console.log(` Used JS heap: ${(memoryAfter.usedJSHeap / 1024 / 1024).toFixed(1)} MB`); console.log(` Total JS heap: ${(memoryAfter.totalJSHeap / 1024 / 1024).toFixed(1)} MB`); if (memory) { const delta = memoryAfter.usedJSHeap - memory.usedJSHeap; console.log(` Heap delta: ${(delta > 0 ? '+' : '')}${(delta / 1024 / 1024).toFixed(1)} MB`); } } // Network summary console.log(`\n--- Network Summary ---`); console.log(` Total requests: ${networkResponses.length}`); const totalNetworkSize = networkResponses.reduce((s, r) => s + parseInt(r.size || '0'), 0); console.log(` Total response: ${(totalNetworkSize / 1024).toFixed(1)} KB`); const failedRequests = networkResponses.filter(r => r.status >= 400); if (failedRequests.length > 0) { console.log(` Failed requests: ${failedRequests.length}`); for (const r of failedRequests) { console.log(` [${r.status}] ${r.url}`); } } } catch (err) { console.log(`\n ❌ Error during runtime analysis: ${err.message}`); } finally { await browser.close(); } } // ─── Main ─── console.log('╔═══════════════════════════════════════════════╗'); console.log('║ OmniAI Web Preview - Performance Analysis ║'); console.log('╚═══════════════════════════════════════════════╝'); analyzeBundles(); await runtimeAnalysis(); console.log('\n═══════════════════════════════════════════════'); console.log(' ANALYSIS COMPLETE'); console.log('═══════════════════════════════════════════════');