feat: refine generation workspace experience

This commit is contained in:
2026-06-08 13:44:03 +08:00
35 changed files with 5249 additions and 350 deletions
+301
View File
@@ -0,0 +1,301 @@
/**
* Dynamic analysis without Playwright - uses Node.js to analyze module structure,
* dependency graph, import patterns, and potential runtime costs.
*/
import { readFileSync, readdirSync, statSync } from 'fs';
import { join, relative, basename } from 'path';
const SRC = join(import.meta.dirname, '..', 'src');
const DIST = join(import.meta.dirname, '..', 'dist');
const results = [];
function walk(dir) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory() && entry.name !== 'node_modules') walk(full);
else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
const content = readFileSync(full, 'utf-8');
results.push({ file: relative(join(SRC, '..'), full), content, lines: content.split('\n').length });
}
}
}
walk(SRC);
// ─── 1. Dependency Graph Analysis ───
console.log('═══════════════════════════════════════════════');
console.log(' 1. MODULE DEPENDENCY GRAPH ANALYSIS');
console.log('═══════════════════════════════════════════════');
const importMap = new Map(); // file -> [imports]
const importedBy = new Map(); // file -> [importers]
for (const r of results) {
const imports = [];
// Match import statements
const importRe = /import\s+(?:.*?\s+from\s+)?['"]([^'"]+)['"]/g;
let m;
while ((m = importRe.exec(r.content)) !== null) {
imports.push(m[1]);
}
// Match dynamic imports
const dynRe = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
while ((m = dynRe.exec(r.content)) !== null) {
imports.push(`[dynamic]${m[1]}`);
}
importMap.set(r.file, imports);
}
// Find circular dependencies
console.log('\n--- Circular Dependency Detection ---');
function findCircular(file, visited = new Set(), path = []) {
if (visited.has(file)) {
if (path.includes(file)) {
console.log(` [CIRCULAR] ${path.slice(path.indexOf(file)).join(' -> ')} -> ${file}`);
}
return;
}
visited.add(file);
path.push(file);
const deps = importMap.get(file) || [];
for (const dep of deps) {
if (dep.startsWith('.') || dep.startsWith('/')) {
// Resolve relative path
const dir = file.split('/').slice(0, -1).join('/');
const resolved = dep.replace(/^\.\//, dir + '/').replace(/^\.\.\//, '');
// Find matching file
for (const r of results) {
if (r.file.includes(resolved) || r.file.includes(basename(resolved))) {
findCircular(r.file, new Set(visited), [...path]);
}
}
}
}
}
// Check high-fanin files (imported by many)
const fanIn = new Map();
for (const [file, imports] of importMap) {
for (const imp of imports) {
const key = imp.replace(/\[dynamic\]/, '');
fanIn.set(key, (fanIn.get(key) || 0) + 1);
}
}
console.log('\n--- High Fan-In Modules (most imported) ---');
const sortedFanIn = [...fanIn.entries()].sort((a, b) => b[1] - a[1]).slice(0, 15);
for (const [mod, count] of sortedFanIn) {
const bar = '█'.repeat(Math.min(30, count));
console.log(` ${mod.padEnd(50)} ${String(count).padStart(3)}x ${bar}`);
}
// Check high-fanout files (import many)
console.log('\n--- High Fan-Out Modules (import most) ---');
const sortedFanOut = [...importMap.entries()]
.map(([f, imps]) => [f, imps.length])
.sort((a, b) => b[1] - a[1]).slice(0, 15);
for (const [file, count] of sortedFanOut) {
const bar = '█'.repeat(Math.min(30, count));
console.log(` ${file.padEnd(50)} ${String(count).padStart(3)} deps ${bar}`);
}
// Dynamic imports analysis (lazy loading effectiveness)
console.log('\n--- Lazy Loading (Dynamic Imports) ---');
let dynamicImports = 0, staticImports = 0;
for (const [file, imports] of importMap) {
for (const imp of imports) {
if (imp.startsWith('[dynamic]')) dynamicImports++;
else staticImports++;
}
}
console.log(` Static imports: ${staticImports}`);
console.log(` Dynamic imports: ${dynamicImports}`);
console.log(` Lazy load ratio: ${((dynamicImports / (staticImports + dynamicImports)) * 100).toFixed(1)}%`);
// Find files that should be lazy loaded but aren't
const largePages = results.filter(r => r.lines > 500 && r.file.includes('Page'));
for (const r of largePages) {
const isLazyImported = [...importMap.values()].some(imps =>
imps.some(i => i.startsWith('[dynamic]') && i.includes(basename(r.file, '.tsx')))
);
if (!isLazyImported && !r.file.includes('App')) {
// Check if it's referenced in App.tsx
const appContent = results.find(x => x.file === 'src/App.tsx')?.content || '';
if (appContent.includes(basename(r.file, '.tsx'))) {
console.log(` [INFO] ${r.file} (${r.lines} lines) - loaded via App.tsx`);
}
}
}
// ─── 2. React Rendering Cost Analysis ───
console.log('\n═══════════════════════════════════════════════');
console.log(' 2. REACT RENDERING COST ANALYSIS');
console.log('═══════════════════════════════════════════════');
// Count useState/useReducer per component (state update triggers re-render)
console.log('\n--- State Hook Density ---');
for (const r of results) {
if (!r.file.endsWith('.tsx')) continue;
const stateHooks = (r.content.match(/useState\s*[<(]/g) || []).length;
const reducers = (r.content.match(/useReducer\s*[<(]/g) || []).length;
const effects = (r.content.match(/useEffect\s*\(/g) || []).length;
const memos = (r.content.match(/useMemo\s*\(/g) || []).length;
const callbacks = (r.content.match(/useCallback\s*[<(]/g) || []).length;
const refs = (r.content.match(/useRef\s*[<(]/g) || []).length;
const totalHooks = stateHooks + reducers + effects + memos + callbacks + refs;
if (totalHooks > 15) {
const risk = totalHooks > 30 ? '🔴 HIGH' : totalHooks > 20 ? '🟡 MEDIUM' : '🟢 LOW';
console.log(` ${risk} ${r.file}`);
console.log(` useState:${stateHooks} useReducer:${reducers} useEffect:${effects} useMemo:${memos} useCallback:${callbacks} useRef:${refs} (total:${totalHooks})`);
// Check if there are many state updates that could be batched
const setters = r.content.match(/set\w+\(/g) || [];
if (setters.length > 20) {
console.log(` ⚠️ ${setters.length} state setter calls — potential for excessive re-renders`);
}
// Check for missing useMemo on expensive computations
const expensiveInRender = (r.content.match(/\.map\(|\.filter\(|\.reduce\(|\.sort\(/g) || []).length;
if (expensiveInRender > 5 && memos === 0) {
console.log(` ⚠️ ${expensiveInRender} array operations in render body with 0 useMemo`);
}
}
}
// ─── 3. useEffect Dependency Analysis ───
console.log('\n═══════════════════════════════════════════════');
console.log(' 3. useEffect DEPENDENCY ANALYSIS');
console.log('═══════════════════════════════════════════════');
for (const r of results) {
if (!r.file.endsWith('.tsx')) continue;
// Find useEffect with no dependency array (runs every render)
const noDeps = (r.content.match(/useEffect\s*\(\s*\(\)\s*=>\s*\{[\s\S]*?\}\s*\)/g) || []).length;
// Find useEffect with empty deps
const emptyDeps = (r.content.match(/useEffect\s*\(\s*\(\)\s*=>\s*\{[\s\S]*?\}\s*,\s*\[\s*\]\s*\)/g) || []).length;
if (noDeps > 0) {
console.log(` [RENDER-COST] ${r.file}: ${noDeps} useEffect(s) run EVERY render`);
}
}
// ─── 4. Zustand Store Analysis ───
console.log('\n═══════════════════════════════════════════════');
console.log(' 4. ZUSTAND STORE ANALYSIS');
console.log('═══════════════════════════════════════════════');
const storeFiles = results.filter(r => r.file.includes('store') || r.file.includes('Store'));
for (const r of storeFiles) {
const stateFields = (r.content.match(/^\s+\w+:/gm) || []).length;
const actions = (r.content.match(/^\s+\w+\s*:\s*(\(|function|\w+\s*=>)/gm) || []).length;
const subscribers = (r.content.match(/subscribe\s*\(/g) || []).length;
console.log(`\n ${r.file}`);
console.log(` State fields: ~${stateFields}`);
console.log(` Actions: ~${actions}`);
console.log(` Subscribers: ${subscribers}`);
// Check for selector usage patterns
if (r.content.includes('set(') && !r.content.includes('useShallow')) {
console.log(` ⚠️ Uses set() without useShallow — may cause unnecessary re-renders`);
}
}
// ─── 5. Bundle Composition Analysis ───
console.log('\n═══════════════════════════════════════════════');
console.log(' 5. BUNDLE COMPOSITION ANALYSIS');
console.log('═══════════════════════════════════════════════');
if (readdirSync(DIST).includes('assets')) {
const assets = readdirSync(join(DIST, 'assets'));
const jsFiles = assets.filter(f => f.endsWith('.js') && !f.endsWith('.br'));
const cssFiles = assets.filter(f => f.endsWith('.css') && !f.endsWith('.br'));
let totalJs = 0, totalCss = 0;
const chunks = [];
for (const f of jsFiles) {
const size = statSync(join(DIST, 'assets', f)).size;
totalJs += size;
chunks.push({ name: f, sizeKB: size / 1024, type: 'js' });
}
for (const f of cssFiles) {
const size = statSync(join(DIST, 'assets', f)).size;
totalCss += size;
chunks.push({ name: f, sizeKB: size / 1024, type: 'css' });
}
chunks.sort((a, b) => b.sizeKB - a.sizeKB);
console.log(`\n Total JS: ${(totalJs / 1024).toFixed(1)} KB (${(totalJs / 1024 / 1024).toFixed(2)} MB)`);
console.log(` Total CSS: ${(totalCss / 1024).toFixed(1)} KB (${(totalCss / 1024 / 1024).toFixed(2)} MB)`);
console.log(` Total: ${((totalJs + totalCss) / 1024).toFixed(1)} KB`);
// CSS is suspiciously large
const cssRatio = totalCss / totalJs;
if (cssRatio > 0.5) {
console.log(`\n ⚠️ CSS/JS ratio: ${(cssRatio * 100).toFixed(0)}% — CSS bundle may contain unused styles`);
console.log(` Consider: PurgeCSS, CSS Modules, or extracting to separate loads`);
}
// Check initial load budget
const initialChunks = chunks.filter(c =>
c.name.includes('index') || c.name.includes('vendor-react') || c.name.includes('vendor-antd')
);
const initialLoad = initialChunks.reduce((s, c) => s + c.sizeKB, 0);
console.log(`\n Critical path (initial load):`);
for (const c of initialChunks) {
console.log(` ${c.name.padEnd(45)} ${c.sizeKB.toFixed(1).padStart(8)} KB`);
}
console.log(` ${'TOTAL'.padEnd(45)} ${initialLoad.toFixed(1).padStart(8)} KB`);
if (initialLoad > 300) {
console.log(`\n ⚠️ Initial load > 300KB — consider code splitting or tree-shaking`);
}
}
// ─── 6. API Client Efficiency ───
console.log('\n═══════════════════════════════════════════════');
console.log(' 6. API CLIENT EFFICIENCY');
console.log('═══════════════════════════════════════════════');
const apiFiles = results.filter(r => r.file.includes('src/api/'));
for (const r of apiFiles) {
const fetchCalls = (r.content.match(/fetch\s*\(/g) || []).length;
const retries = (r.content.match(/retry|Retry|RETRY/g) || []).length;
const abortSignals = (r.content.match(/AbortSignal|AbortController/g) || []).length;
const timeouts = (r.content.match(/timeout|Timeout|setTimeout/g) || []).length;
if (fetchCalls > 0) {
console.log(`\n ${r.file}`);
console.log(` fetch calls: ${fetchCalls}`);
console.log(` retry logic: ${retries > 0 ? '✅' : '❌ none'}`);
console.log(` abort signals: ${abortSignals > 0 ? '✅' : '❌ none'}`);
console.log(` timeouts: ${timeouts}`);
}
}
// ─── 7. TypeScript Compilation Metrics ───
console.log('\n═══════════════════════════════════════════════');
console.log(' 7. TYPE COMPLEXITY');
console.log('═══════════════════════════════════════════════');
let totalTypes = 0, totalInterfaces = 0, totalEnums = 0, totalGenerics = 0;
for (const r of results) {
totalTypes += (r.content.match(/^export\s+type\s+/gm) || []).length;
totalTypes += (r.content.match(/^type\s+/gm) || []).length;
totalInterfaces += (r.content.match(/^export\s+interface\s+/gm) || []).length;
totalInterfaces += (r.content.match(/^interface\s+/gm) || []).length;
totalEnums += (r.content.match(/enum\s+\w+/g) || []).length;
totalGenerics += (r.content.match(/<\w+(?:\s*,\s*\w+)*>/g) || []).length;
}
console.log(` Type aliases: ${totalTypes}`);
console.log(` Interfaces: ${totalInterfaces}`);
console.log(` Enums: ${totalEnums}`);
console.log(` Generic usages: ${totalGenerics}`);
console.log(` Total TS files: ${results.length}`);
// ─── Final Summary ───
console.log('\n═══════════════════════════════════════════════');
console.log(' ANALYSIS COMPLETE');
console.log('═══════════════════════════════════════════════');
+305
View File
@@ -0,0 +1,305 @@
/**
* Dynamic performance analysis using Playwright.
* Measures: page load, bundle sizes, memory, rendering, network.
*/
import { chromium } from 'playwright';
import { readFileSync, 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;
let 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('╚═══════════════════════════════════════════════╝');
const bundleResult = analyzeBundles();
await runtimeAnalysis();
console.log('\n═══════════════════════════════════════════════');
console.log(' ANALYSIS COMPLETE');
console.log('═══════════════════════════════════════════════');
+148
View File
@@ -0,0 +1,148 @@
import { readdirSync, readFileSync, statSync } from 'fs';
import { join, relative } from 'path';
const SRC = join(import.meta.dirname, '..', 'src');
const results = [];
function walk(dir) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory() && entry.name !== 'node_modules') walk(full);
else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
const content = readFileSync(full, 'utf-8');
const lines = content.split('\n').length;
results.push({ file: relative(join(SRC, '..'), full), lines, content });
}
}
}
walk(SRC);
results.sort((a, b) => b.lines - a.lines);
console.log('=== TOP 30 FILES BY LINE COUNT ===');
for (const r of results.slice(0, 30)) {
console.log(`${String(r.lines).padStart(5)} ${r.file}`);
}
// Detect nested loops (3+ levels)
console.log('\n=== NESTED LOOP DETECTION (3+ levels) ===');
const loopPatterns = [
/for\s*\(/g, /while\s*\(/g, /\.forEach\s*\(/g, /\.map\s*\(/g,
/\.filter\s*\(/g, /\.reduce\s*\(/g, /\.some\s*\(/g, /\.every\s*\(/g,
/\.flatMap\s*\(/g, /\.find\s*\(/g
];
for (const r of results) {
const lines = r.content.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
let loopCount = 0;
for (const p of loopPatterns) {
p.lastIndex = 0;
if (p.test(line)) loopCount++;
}
// Check surrounding context for nesting
if (loopCount > 0 || /for\s*\(/.test(line) || /\.map\(/.test(line) || /\.forEach\(/.test(line) || /\.filter\(/.test(line) || /\.reduce\(/.test(line)) {
// Count loop keywords on this single line
let singleLineLoops = 0;
for (const p of loopPatterns) {
p.lastIndex = 0;
const matches = line.match(new RegExp(p.source, 'g'));
if (matches) singleLineLoops += matches.length;
}
if (singleLineLoops >= 2) {
console.log(` [NESTED] ${r.file}:${i + 1} (${singleLineLoops} loops on one line)`);
console.log(` ${line.trim().substring(0, 120)}`);
}
}
}
}
// Detect missing cleanup in useEffect
console.log('\n=== MEMORY LEAK RISK: useEffect without cleanup ===');
for (const r of results) {
if (!r.file.endsWith('.tsx') && !r.file.endsWith('.ts')) continue;
const content = r.content;
// Find useEffect blocks that contain setInterval/setTimeout/addEventListener but no return
const useEffectRegex = /useEffect\s*\(\s*\(\)\s*=>\s*\{([\s\S]*?)\}\s*,/g;
let match;
while ((match = useEffectRegex.exec(content)) !== null) {
const body = match[1];
const hasTimer = /setInterval|setTimeout/.test(body);
const hasListener = /addEventListener/.test(body);
const hasSubscribe = /\.subscribe\(/.test(body);
const hasCleanup = /return\s*\(\)\s*=>|return\s*function|return\s*\(\{/.test(body);
if ((hasTimer || hasListener || hasSubscribe) && !hasCleanup) {
const lineNum = content.substring(0, match.index).split('\n').length;
console.log(` [RISK] ${r.file}:${lineNum}`);
if (hasTimer) console.log(` - Has setInterval/setTimeout without cleanup`);
if (hasListener) console.log(` - Has addEventListener without cleanup`);
if (hasSubscribe) console.log(` - Has subscribe without cleanup`);
console.log(` ${body.trim().substring(0, 200)}`);
console.log('');
}
}
}
// Detect objects/arrays/functions created in render body (not memoized)
console.log('\n=== REDUNDANT COMPUTATION: Non-memoized values in components ===');
for (const r of results) {
if (!r.file.endsWith('.tsx')) continue;
const lines = r.content.split('\n');
// Look for const x = [...], const x = {...}, const x = (...) => patterns outside useMemo
let inUseMemo = 0;
for (let i = 0; i < lines.length; i++) {
if (/useMemo\s*\(/.test(lines[i])) inUseMemo++;
if (inUseMemo > 0 && /\)/.test(lines[i])) {
// Rough heuristic - not perfect
}
if (inUseMemo === 0) {
// Expensive operations in render
if (/\.map\s*\(.*\.map\s*\(/.test(lines[i])) {
console.log(` [PERF] ${r.file}:${i + 1} - Chained .map calls in render`);
console.log(` ${lines[i].trim().substring(0, 120)}`);
}
if (/\.filter\s*\(.*\.map\s*\(/.test(lines[i]) || /\.map\s*\(.*\.filter\s*\(/.test(lines[i])) {
console.log(` [PERF] ${r.file}:${i + 1} - filter+map chain in render`);
console.log(` ${lines[i].trim().substring(0, 120)}`);
}
}
}
}
// Detect deeply nested conditionals (4+ levels)
console.log('\n=== HIGH COMPLEXITY: Deep nesting ===');
for (const r of results) {
const lines = r.content.split('\n');
let maxIndent = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.trim() === '') continue;
const indent = line.match(/^(\s*)/)[1].length;
// Only flag if inside if/else/switch/ternary
if (indent >= 16 && /if\s*\(|else|switch\s*\(|case\s+/.test(line.trim())) {
console.log(` [DEEP] ${r.file}:${i + 1} (indent=${indent})`);
console.log(` ${line.trim().substring(0, 120)}`);
}
}
}
// Detect inline style objects in JSX (recreated every render)
console.log('\n=== REDUNDANT: Inline style objects in JSX ===');
for (const r of results) {
if (!r.file.endsWith('.tsx')) continue;
const lines = r.content.split('\n');
for (let i = 0; i < lines.length; i++) {
if (/style\s*=\s*\{\s*\{/.test(lines[i]) && !/useMemo/.test(lines[i])) {
console.log(` [INLINE] ${r.file}:${i + 1}`);
console.log(` ${lines[i].trim().substring(0, 120)}`);
}
}
}
// Total stats
console.log('\n=== SUMMARY ===');
console.log(`Total files: ${results.length}`);
console.log(`Total lines: ${results.reduce((s, r) => s + r.lines, 0)}`);
console.log(`Files > 500 lines: ${results.filter(r => r.lines > 500).length}`);
console.log(`Files > 1000 lines: ${results.filter(r => r.lines > 1000).length}`);