analyze-bundle.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. /**
  2. * Bundle 分析脚本
  3. *
  4. * 自动化 bundle 分析流程:
  5. * 1. 构建生产版本
  6. * 2. 生成分析报告
  7. * 3. 输出关键指标
  8. *
  9. * 使用: node scripts/analyze-bundle.js
  10. */
  11. import { execSync } from 'node:child_process';
  12. import fs from 'node:fs';
  13. import path from 'node:path';
  14. // ANSI 颜色代码
  15. const colors = {
  16. reset: '\x1b[0m',
  17. bright: '\x1b[1m',
  18. green: '\x1b[32m',
  19. yellow: '\x1b[33m',
  20. blue: '\x1b[34m',
  21. red: '\x1b[31m',
  22. };
  23. function log(message, color = 'reset') {
  24. console.log(`${colors[color]}${message}${colors.reset}`);
  25. }
  26. function formatBytes(bytes) {
  27. if (bytes < 1024) return bytes + ' B';
  28. if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
  29. return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
  30. }
  31. async function main() {
  32. log('\n🔍 Bundle 分析工具\n', 'bright');
  33. // 步骤 1: 清理旧的构建产物
  34. log('📦 清理旧的构建产物...', 'blue');
  35. try {
  36. if (fs.existsSync('dist')) {
  37. fs.rmSync('dist', { recursive: true, force: true });
  38. }
  39. log('✅ 清理完成\n', 'green');
  40. } catch (error) {
  41. log(`❌ 清理失败: ${error.message}\n`, 'red');
  42. }
  43. // 步骤 2: 构建生产版本
  44. log('🏗️ 构建生产版本...', 'blue');
  45. try {
  46. execSync('npm run build', {
  47. stdio: 'inherit',
  48. env: { ...process.env, ANALYZE: 'true' }
  49. });
  50. log('✅ 构建完成\n', 'green');
  51. } catch (error) {
  52. log('❌ 构建失败,请检查错误信息\n', 'red');
  53. process.exit(1);
  54. }
  55. // 步骤 3: 分析构建产物
  56. log('📊 分析构建产物...\n', 'blue');
  57. const distPath = path.join(process.cwd(), 'dist', 'assets');
  58. if (!fs.existsSync(distPath)) {
  59. log('❌ dist/assets 目录不存在\n', 'red');
  60. return;
  61. }
  62. const files = fs.readdirSync(distPath);
  63. // 分类统计
  64. const stats = {
  65. js: [],
  66. css: [],
  67. other: [],
  68. };
  69. files.forEach((file) => {
  70. const filePath = path.join(distPath, file);
  71. const stat = fs.statSync(filePath);
  72. const size = stat.size;
  73. const fileInfo = {
  74. name: file,
  75. size,
  76. sizeFormatted: formatBytes(size),
  77. };
  78. if (file.endsWith('.js')) {
  79. stats.js.push(fileInfo);
  80. } else if (file.endsWith('.css')) {
  81. stats.css.push(fileInfo);
  82. } else {
  83. stats.other.push(fileInfo);
  84. }
  85. });
  86. // 排序(按大小降序)
  87. stats.js.sort((a, b) => b.size - a.size);
  88. stats.css.sort((a, b) => b.size - a.size);
  89. // 输出 JS 文件统计
  90. log('JavaScript 文件:', 'bright');
  91. stats.js.forEach((file, index) => {
  92. const color = file.size > 600 * 1024 ? 'red' : file.size > 200 * 1024 ? 'yellow' : 'green';
  93. const icon = file.size > 600 * 1024 ? '⚠️ ' : ' ';
  94. log(`${icon}${index + 1}. ${file.name.padEnd(50)} ${file.sizeFormatted}`, color);
  95. });
  96. const totalJS = stats.js.reduce((sum, file) => sum + file.size, 0);
  97. log(`\n总计: ${formatBytes(totalJS)}\n`, 'bright');
  98. // 输出 CSS 文件统计
  99. log('CSS 文件:', 'bright');
  100. stats.css.forEach((file, index) => {
  101. log(` ${index + 1}. ${file.name.padEnd(50)} ${file.sizeFormatted}`, 'green');
  102. });
  103. const totalCSS = stats.css.reduce((sum, file) => sum + file.size, 0);
  104. log(`\n总计: ${formatBytes(totalCSS)}\n`, 'bright');
  105. // 总体统计
  106. const total = totalJS + totalCSS;
  107. log('📈 总体统计:', 'bright');
  108. log(` JavaScript: ${formatBytes(totalJS)} (${((totalJS / total) * 100).toFixed(1)}%)`, 'blue');
  109. log(` CSS: ${formatBytes(totalCSS)} (${((totalCSS / total) * 100).toFixed(1)}%)`, 'blue');
  110. log(` 总计: ${formatBytes(total)}\n`, 'bright');
  111. // 警告信息
  112. const largeFiles = stats.js.filter(f => f.size > 600 * 1024);
  113. if (largeFiles.length > 0) {
  114. log('⚠️ 警告: 以下文件超过 600 KB,建议优化:', 'yellow');
  115. largeFiles.forEach((file) => {
  116. log(` - ${file.name}: ${file.sizeFormatted}`, 'yellow');
  117. });
  118. log('');
  119. }
  120. // 优化建议
  121. log('💡 优化建议:', 'bright');
  122. if (largeFiles.length > 0) {
  123. log(' 1. 检查是否可以进一步拆分大型 chunk', 'blue');
  124. log(' 2. 确认是否启用了 tree-shaking', 'blue');
  125. log(' 3. 考虑使用动态导入延迟加载', 'blue');
  126. } else {
  127. log(' ✅ Bundle 大小在合理范围内', 'green');
  128. }
  129. // 检查是否生成了分析报告
  130. const statsFile = path.join(process.cwd(), 'dist', 'stats.html');
  131. if (fs.existsSync(statsFile)) {
  132. log('\n📊 详细分析报告已生成: dist/stats.html', 'green');
  133. log(' 在浏览器中打开此文件查看可视化分析\n', 'blue');
  134. } else {
  135. log('\n⚠️ 未生成详细分析报告', 'yellow');
  136. log(' 请确保已安装 rollup-plugin-visualizer:', 'yellow');
  137. log(' npm install --save-dev rollup-plugin-visualizer\n', 'yellow');
  138. }
  139. }
  140. // 运行脚本
  141. main().catch((error) => {
  142. console.error('Error:', error);
  143. process.exit(1);
  144. });