Bundle analysis and monitoring
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
#!/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;
|
||||
Reference in New Issue
Block a user