#!/usr/bin/env node /** * Bundle Analysis Script * Analyzes webpack bundles and provides detailed performance insights */ const { execSync } = require("child_process"); const fs = require("fs"); const path = require("path"); const BUNDLE_ANALYSIS_DIR = path.join(__dirname, "..", ".next", "analyze"); const PERFORMANCE_BUDGETS = require("../performance-budgets.json"); class BundleAnalyzer { constructor() { this.results = { timestamp: new Date().toISOString(), bundles: {}, recommendations: [], budgetViolations: [], }; } /** * Run bundle analysis using build output */ async analyzeBundles() { console.log("🔍 Starting bundle analysis..."); try { // Ensure analyze directory exists if (!fs.existsSync(BUNDLE_ANALYSIS_DIR)) { fs.mkdirSync(BUNDLE_ANALYSIS_DIR, { recursive: true }); } // Build the project first console.log("🏗️ Building project..."); execSync("npm run build", { stdio: "inherit" }); // Parse bundle stats from build output await this.parseBundleStats(); // Check performance budgets this.checkPerformanceBudgets(); // Generate recommendations this.generateRecommendations(); // Save results this.saveResults(); console.log("✅ Bundle analysis complete!"); console.log(`📁 Results saved to: ${BUNDLE_ANALYSIS_DIR}`); } catch (error) { console.error("❌ Bundle analysis failed:", error.message); process.exit(1); } } /** * Parse bundle statistics from build output */ async parseBundleStats() { const staticPath = path.join(__dirname, "..", ".next", "static"); const chunksPath = path.join(staticPath, "chunks"); // Analyze static assets if (fs.existsSync(staticPath)) { this.analyzeDirectory(staticPath, "static"); } // Analyze chunks if (fs.existsSync(chunksPath)) { this.analyzeDirectory(chunksPath, "chunks"); } // Analyze pages const pagesPath = path.join(__dirname, "..", ".next", "server", "pages"); if (fs.existsSync(pagesPath)) { this.analyzeDirectory(pagesPath, "pages"); } } /** * Analyze directory for bundle sizes */ analyzeDirectory(dirPath, type) { const files = fs.readdirSync(dirPath); files.forEach((file) => { const filePath = path.join(dirPath, file); const stats = fs.statSync(filePath); if (stats.isFile() && (file.endsWith(".js") || file.endsWith(".css"))) { const key = `${type}/${file}`; this.results.bundles[key] = { size: stats.size, sizeKB: Math.round(stats.size / 1024), lastModified: stats.mtime, type: file.endsWith(".css") ? "css" : "js", }; } else if (stats.isDirectory()) { this.analyzeDirectory(filePath, `${type}/${file}`); } }); } /** * Check against performance budgets */ checkPerformanceBudgets() { const budgets = PERFORMANCE_BUDGETS.budgets || []; Object.entries(this.results.bundles).forEach(([filename, bundle]) => { const budget = budgets.find( (b) => filename.includes(b.name) || b.name === "all" ); if (budget) { if (bundle.sizeKB > budget.maxSizeKB) { this.results.budgetViolations.push({ file: filename, currentSize: bundle.sizeKB, maxSize: budget.maxSizeKB, overage: bundle.sizeKB - budget.maxSizeKB, severity: bundle.sizeKB > budget.maxSizeKB * 1.2 ? "high" : "medium", }); } } else { // Default budget check for large files if (bundle.sizeKB > 500) { this.results.budgetViolations.push({ file: filename, currentSize: bundle.sizeKB, maxSize: 500, overage: bundle.sizeKB - 500, severity: bundle.sizeKB > 600 ? "high" : "medium", }); } } }); } /** * Generate optimization recommendations */ generateRecommendations() { const recommendations = []; // Check for large bundles Object.entries(this.results.bundles).forEach(([filename, bundle]) => { if (bundle.sizeKB > 500) { recommendations.push({ type: "large-bundle", file: filename, size: bundle.sizeKB, suggestion: "Consider code splitting or dynamic imports for this bundle", }); } }); // Check for budget violations if (this.results.budgetViolations.length > 0) { recommendations.push({ type: "budget-violation", count: this.results.budgetViolations.length, suggestion: "Review and optimize bundles that exceed performance budgets", }); } // General recommendations const totalSize = Object.values(this.results.bundles).reduce( (sum, bundle) => sum + bundle.sizeKB, 0 ); if (totalSize > 2000) { recommendations.push({ type: "total-size", size: totalSize, suggestion: "Consider implementing more aggressive code splitting", }); } this.results.recommendations = recommendations; } /** * Save analysis results */ saveResults() { // Ensure directory exists if (!fs.existsSync(BUNDLE_ANALYSIS_DIR)) { fs.mkdirSync(BUNDLE_ANALYSIS_DIR, { recursive: true }); } const resultsPath = path.join(BUNDLE_ANALYSIS_DIR, "bundle-analysis.json"); fs.writeFileSync(resultsPath, JSON.stringify(this.results, null, 2)); // Generate markdown report this.generateMarkdownReport(); } /** * Generate markdown report */ generateMarkdownReport() { const reportPath = path.join(BUNDLE_ANALYSIS_DIR, "bundle-report.md"); let report = `# Bundle Analysis Report\n\n`; report += `**Generated:** ${this.results.timestamp}\n\n`; // Bundle sizes report += `## Bundle Sizes\n\n`; report += `| File | Size (KB) | Status |\n`; report += `|------|-----------|--------|\n`; Object.entries(this.results.bundles).forEach(([filename, bundle]) => { const status = bundle.sizeKB > 500 ? "⚠️ Large" : "✅ Good"; report += `| ${filename} | ${bundle.sizeKB} | ${status} |\n`; }); // Budget violations if (this.results.budgetViolations.length > 0) { report += `\n## Budget Violations\n\n`; this.results.budgetViolations.forEach((violation) => { report += `- **${violation.file}**: ${violation.currentSize}KB (exceeds ${violation.maxSize}KB by ${violation.overage}KB)\n`; }); } // Recommendations if (this.results.recommendations.length > 0) { report += `\n## Recommendations\n\n`; this.results.recommendations.forEach((rec) => { report += `- ${rec.suggestion}\n`; }); } fs.writeFileSync(reportPath, report); } } // Run analysis if called directly if (require.main === module) { const analyzer = new BundleAnalyzer(); analyzer.analyzeBundles().catch(console.error); } module.exports = BundleAnalyzer;