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
JavaScript
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');
}
}