f2cdb6fec9
CI Pipeline / test (20) (pull_request) Successful in 6m27s
CI Pipeline / test (18) (pull_request) Successful in 8m15s
CI Pipeline / e2e (firefox) (pull_request) Successful in 3m22s
CI Pipeline / e2e (webkit) (pull_request) Successful in 3m39s
CI Pipeline / e2e (chromium) (pull_request) Successful in 11m31s
CI Pipeline / visual-regression (pull_request) Successful in 6m22s
CI Pipeline / storybook (pull_request) Successful in 1m26s
CI Pipeline / performance (pull_request) Successful in 6m44s
CI Pipeline / build (pull_request) Successful in 2m8s
254 lines
7.0 KiB
JavaScript
254 lines
7.0 KiB
JavaScript
#!/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;
|