UNPKG

vibe-code-build

Version:

Real-time code monitoring with teaching explanations, CLAUDE.md compliance checking, and interactive chat

551 lines (470 loc) 18.5 kB
import fs from 'fs/promises'; import path from 'path'; import chalk from 'chalk'; import ora from 'ora'; import { gzipSync } from 'zlib'; import { SEOChecker } from './seo-checker.js'; export class PerformanceOptimizer { constructor(projectPath = process.cwd(), options = {}) { this.projectPath = projectPath; this.options = options; this.results = { bundleSize: null, imageOptimization: null, codeOptimization: null, seo: null, performance: null }; } async checkAll() { const spinner = this.options.silent ? null : ora('Running performance optimization checks...').start(); try { if (spinner) spinner.text = 'Analyzing bundle sizes...'; this.results.bundleSize = await this.checkBundleSize(); if (spinner) spinner.text = 'Checking image optimization...'; this.results.imageOptimization = await this.checkImageOptimization(); if (spinner) spinner.text = 'Analyzing code optimization...'; this.results.codeOptimization = await this.checkCodeOptimization(); if (spinner) spinner.text = 'Checking SEO optimization...'; this.results.seo = await this.checkSEO(); if (spinner) spinner.text = 'Analyzing performance metrics...'; this.results.performance = await this.checkPerformance(); if (spinner) spinner.succeed('Performance checks completed'); return this.results; } catch (error) { if (spinner) spinner.fail('Performance checks failed'); throw error; } } async checkBundleSize() { try { const distPaths = ['dist', 'build', 'out', '.next']; let bundleInfo = { totalSize: 0, files: [], largeFiles: [] }; for (const distPath of distPaths) { const fullPath = path.join(this.projectPath, distPath); try { await fs.access(fullPath); const files = await this.analyzeDirectory(fullPath); bundleInfo.files.push(...files); } catch {} } bundleInfo.totalSize = bundleInfo.files.reduce((sum, file) => sum + file.size, 0); bundleInfo.largeFiles = bundleInfo.files .filter(file => file.size > 500 * 1024) .sort((a, b) => b.size - a.size); const jsFiles = bundleInfo.files.filter(f => f.name.endsWith('.js')); const cssFiles = bundleInfo.files.filter(f => f.name.endsWith('.css')); return { status: bundleInfo.totalSize > 10 * 1024 * 1024 ? 'warning' : 'passed', message: `Total bundle size: ${this.formatSize(bundleInfo.totalSize)}`, totalSize: bundleInfo.totalSize, breakdown: { js: { count: jsFiles.length, size: jsFiles.reduce((sum, f) => sum + f.size, 0) }, css: { count: cssFiles.length, size: cssFiles.reduce((sum, f) => sum + f.size, 0) }, other: { count: bundleInfo.files.length - jsFiles.length - cssFiles.length, size: bundleInfo.totalSize - jsFiles.reduce((sum, f) => sum + f.size, 0) - cssFiles.reduce((sum, f) => sum + f.size, 0) } }, largeFiles: bundleInfo.largeFiles.slice(0, 5).map(f => ({ name: f.name, size: this.formatSize(f.size), gzipSize: this.formatSize(f.gzipSize) })), recommendations: this.getBundleSizeRecommendations(bundleInfo) }; } catch (error) { return { status: 'skipped', message: 'No build output found', error: error.message }; } } async checkImageOptimization() { try { const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp']; const images = await this.findFiles(imageExtensions); const imageAnalysis = []; let totalSize = 0; let unoptimizedCount = 0; for (const imagePath of images.slice(0, 50)) { try { const stats = await fs.stat(imagePath); const size = stats.size; totalSize += size; const ext = path.extname(imagePath).toLowerCase(); const name = path.basename(imagePath); const analysis = { path: path.relative(this.projectPath, imagePath), size: size, format: ext, optimized: true, recommendations: [] }; if (size > 1024 * 1024 && (ext === '.jpg' || ext === '.jpeg' || ext === '.png')) { analysis.optimized = false; analysis.recommendations.push('Image is over 1MB - consider compression'); unoptimizedCount++; } if ((ext === '.png' || ext === '.jpg' || ext === '.jpeg') && !imagePath.includes('.webp')) { analysis.recommendations.push('Consider converting to WebP format'); } if (size > 200 * 1024 && ext === '.svg') { analysis.recommendations.push('SVG is large - consider optimization with SVGO'); } if (analysis.recommendations.length > 0) { imageAnalysis.push(analysis); } } catch {} } return { status: unoptimizedCount > 10 ? 'warning' : 'passed', message: `Found ${images.length} images (${unoptimizedCount} need optimization)`, totalImages: images.length, totalSize: this.formatSize(totalSize), unoptimized: unoptimizedCount, recommendations: imageAnalysis.slice(0, 10).map(img => ({ file: img.path, size: this.formatSize(img.size), suggestions: img.recommendations })) }; } catch (error) { return { status: 'error', message: 'Failed to check images', error: error.message }; } } async checkCodeOptimization() { const findings = []; try { const jsFiles = await this.findFiles(['.js', '.jsx', '.ts', '.tsx']); for (const file of jsFiles.slice(0, 50)) { try { const content = await fs.readFile(file, 'utf8'); const fileFindings = []; if (content.includes('console.log') && !file.includes('test')) { fileFindings.push({ type: 'console.log', severity: 'medium', message: 'Remove console.log statements in production' }); } const importMatches = content.match(/import\s+{([^}]+)}\s+from/g) || []; for (const match of importMatches) { if (match.split(',').length > 10) { fileFindings.push({ type: 'large-import', severity: 'low', message: 'Consider breaking up large imports' }); } } if (content.includes('require(') && content.includes('import ')) { fileFindings.push({ type: 'mixed-modules', severity: 'medium', message: 'Mixed CommonJS and ES6 modules' }); } const longLines = content.split('\n').filter(line => line.length > 120); if (longLines.length > 10) { fileFindings.push({ type: 'long-lines', severity: 'low', message: `${longLines.length} lines exceed 120 characters` }); } if (content.includes('// TODO') || content.includes('// FIXME')) { fileFindings.push({ type: 'todo-comments', severity: 'info', message: 'Contains TODO/FIXME comments' }); } if (fileFindings.length > 0) { findings.push({ file: path.relative(this.projectPath, file), issues: fileFindings }); } } catch {} } const totalIssues = findings.reduce((sum, f) => sum + f.issues.length, 0); return { status: totalIssues > 50 ? 'warning' : 'passed', message: `Found ${totalIssues} code optimization opportunities`, totalIssues, findings: findings.slice(0, 10), summary: { consoleLog: findings.filter(f => f.issues.some(i => i.type === 'console.log')).length, mixedModules: findings.filter(f => f.issues.some(i => i.type === 'mixed-modules')).length, todos: findings.filter(f => f.issues.some(i => i.type === 'todo-comments')).length } }; } catch (error) { return { status: 'error', message: 'Failed to check code optimization', error: error.message }; } } async checkSEO() { // Delegate to the comprehensive SEO checker const seoChecker = new SEOChecker(this.projectPath, this.options); const seoResults = await seoChecker.checkAll(); // Transform results to match the expected format return { status: seoResults.overall?.status || 'unknown', message: seoResults.overall?.message || 'SEO check completed', score: seoResults.overall?.score, grade: seoResults.overall?.grade, categories: { technical: seoResults.technical, content: seoResults.content, social: seoResults.social, performance: seoResults.performance }, overall: seoResults.overall, // For backward compatibility totalIssues: seoResults.overall?.summary?.totalIssues || 0, issues: this.extractTopIssues(seoResults), summary: seoResults.overall?.summary || {}, recommendations: seoResults.overall?.recommendations || [] }; } extractTopIssues(seoResults) { const allIssues = []; ['technical', 'content', 'social', 'performance'].forEach(category => { if (seoResults[category]?.issues) { seoResults[category].issues.forEach(issue => { allIssues.push({ ...issue, category }); }); } }); // Sort by severity and return top issues const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 }; return allIssues .sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]) .slice(0, 20); } async checkPerformance() { const performanceIssues = []; try { const jsFiles = await this.findFiles(['.js', '.jsx', '.ts', '.tsx']); for (const file of jsFiles.slice(0, 30)) { try { const content = await fs.readFile(file, 'utf8'); if (content.includes('setInterval') && !content.includes('clearInterval')) { performanceIssues.push({ type: 'uncleaned-interval', severity: 'high', file: path.relative(this.projectPath, file), message: 'setInterval without cleanup' }); } if (content.includes('addEventListener') && !content.includes('removeEventListener')) { performanceIssues.push({ type: 'uncleaned-listener', severity: 'medium', file: path.relative(this.projectPath, file), message: 'Event listener without cleanup' }); } const syncFsOps = ['readFileSync', 'writeFileSync', 'readdirSync']; for (const op of syncFsOps) { if (content.includes(op)) { performanceIssues.push({ type: 'sync-operation', severity: 'high', file: path.relative(this.projectPath, file), message: `Synchronous operation: ${op}` }); break; } } if (content.match(/for\s*\([^)]+\)\s*{[\s\S]*?for\s*\([^)]+\)\s*{/)) { performanceIssues.push({ type: 'nested-loops', severity: 'medium', file: path.relative(this.projectPath, file), message: 'Nested loops detected' }); } if (content.includes('JSON.parse') && content.includes('JSON.stringify')) { const parseCount = (content.match(/JSON\.parse/g) || []).length; const stringifyCount = (content.match(/JSON\.stringify/g) || []).length; if (parseCount + stringifyCount > 5) { performanceIssues.push({ type: 'excessive-json', severity: 'low', file: path.relative(this.projectPath, file), message: `Excessive JSON operations (${parseCount + stringifyCount})` }); } } } catch {} } const cssFiles = await this.findFiles(['.css', '.scss', '.sass']); for (const file of cssFiles.slice(0, 10)) { try { const content = await fs.readFile(file, 'utf8'); const size = Buffer.byteLength(content); if (size > 100 * 1024) { performanceIssues.push({ type: 'large-css', severity: 'medium', file: path.relative(this.projectPath, file), message: `Large CSS file: ${this.formatSize(size)}` }); } if (content.match(/!important/g)?.length > 20) { performanceIssues.push({ type: 'excessive-important', severity: 'low', file: path.relative(this.projectPath, file), message: 'Excessive use of !important' }); } } catch {} } return { status: performanceIssues.filter(i => i.severity === 'high').length > 5 ? 'warning' : 'passed', message: `Found ${performanceIssues.length} performance issues`, totalIssues: performanceIssues.length, issues: performanceIssues.slice(0, 10), summary: { high: performanceIssues.filter(i => i.severity === 'high').length, medium: performanceIssues.filter(i => i.severity === 'medium').length, low: performanceIssues.filter(i => i.severity === 'low').length }, recommendations: this.getPerformanceRecommendations(performanceIssues) }; } catch (error) { return { status: 'error', message: 'Failed to check performance', error: error.message }; } } async analyzeDirectory(dir) { const files = []; async function scan(currentDir) { try { const entries = await fs.readdir(currentDir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentDir, entry.name); if (entry.isFile()) { const stats = await fs.stat(fullPath); const content = await fs.readFile(fullPath); const gzipSize = gzipSync(content).length; files.push({ name: path.relative(dir, fullPath), size: stats.size, gzipSize: gzipSize }); } else if (entry.isDirectory() && !entry.name.startsWith('.')) { await scan(fullPath); } } } catch {} } await scan(dir); return files; } async findFiles(extensions) { const files = []; async function scan(dir) { try { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules' && entry.name !== 'dist' && entry.name !== 'build') { await scan(fullPath); } else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) { files.push(fullPath); } } } catch {} } await scan(this.projectPath); return files; } formatSize(bytes) { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } getBundleSizeRecommendations(bundleInfo) { const recommendations = []; if (bundleInfo.totalSize > 5 * 1024 * 1024) { recommendations.push('Consider code splitting to reduce initial bundle size'); } if (bundleInfo.largeFiles.length > 0) { recommendations.push('Large files detected - consider lazy loading or dynamic imports'); } const jsSize = bundleInfo.files .filter(f => f.name.endsWith('.js')) .reduce((sum, f) => sum + f.size, 0); if (jsSize > 2 * 1024 * 1024) { recommendations.push('JavaScript bundle is large - review dependencies and tree shaking'); } return recommendations; } getPerformanceRecommendations(issues) { const recommendations = []; if (issues.filter(i => i.type === 'sync-operation').length > 0) { recommendations.push('Replace synchronous file operations with async alternatives'); } if (issues.filter(i => i.type === 'uncleaned-interval').length > 0) { recommendations.push('Clean up intervals and timers to prevent memory leaks'); } if (issues.filter(i => i.type === 'nested-loops').length > 3) { recommendations.push('Optimize nested loops - consider using maps or more efficient algorithms'); } return recommendations; } formatResults() { const sections = []; for (const [check, result] of Object.entries(this.results)) { if (!result) continue; const icon = result.status === 'passed' ? '✅' : result.status === 'failed' ? '❌' : result.status === 'warning' ? '⚠️' : result.status === 'skipped' ? '⏭️' : '❓'; const color = result.status === 'passed' ? chalk.green : result.status === 'failed' ? chalk.red : result.status === 'warning' ? chalk.yellow : result.status === 'skipped' ? chalk.gray : chalk.gray; sections.push(color(`${icon} ${check.replace(/([A-Z])/g, ' $1').toUpperCase()}: ${result.message}`)); if (result.recommendations && result.recommendations.length > 0) { sections.push(chalk.gray(` 💡 ${result.recommendations[0]}`)); } } return sections.join('\n'); } }