306 lines
12 KiB
JavaScript
306 lines
12 KiB
JavaScript
/**
|
|
* 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('═══════════════════════════════════════════════');
|