Files
omniai-web/scripts/dynamic-analysis.mjs
stringadmin 4a298d205b
Web Quality / verify (push) Has been cancelled
chore: reduce frontend lint warnings
2026-06-09 12:02:30 +08:00

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('═══════════════════════════════════════════════');