本文时间跨度较大,阶段一距离阶段二有长达三个月之久。阶段一准备时间较短,主要目的就是缩减打包时间,阅读时可以单独看待;阶段二较全面,阶段之间互有补充和影响,若阅读时发现问题期待多多讨论。
阶段二:打包体积减小
此处距离上文已三月之久,当前项目信息如下:
入口文件总大小: 42.2 MB (84.4 MB 未压缩)
JavaScript 模块: 54.7 MB
node_modules: 19.3 MB (4978 个模块)
src: 35.4 MiB (3838 个模块)
CSS 模块: 667 KB
antd: 623 KB
src: 23.8 KB
swiper: 20.1 KB
资源文件: 21.2 MB (304 个资源)
JSON 模块: 1.16 MB (84 个模块)
SVG 资源: 1.17 MB (JavaScript) + 3.35 MB (Asset) (799 个模块)
图片资源: 17.5 KB (JavaScript) + 5.72 MB (Asset) (217 个模块)
当前打包时间:156035 ms
源文件体积分析
const fs = require('fs');
const path = require('path');
/**
* 格式化文件大小显示
* @param {number} bytes - 字节数
* @returns {string} 格式化后的大小字符串
*/
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* 递归遍历目录,收集文件信息
* @param {string} dir - 目录路径
* @param {Array} files - 收集文件信息的数组
*/
function traverseDirectory(dir, files) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// 递归遍历子目录
traverseDirectory(fullPath, files);
} else if (entry.isFile()) {
// 获取文件信息
const stats = fs.statSync(fullPath);
files.push({
path: fullPath,
size: stats.size,
formattedSize: formatFileSize(stats.size)
});
}
}
}
/**
* 分析项目src目录下的文件体积
*/
function analyzeFileSizes() {
const projectRoot = path.join(__dirname, '..');
const srcDir = path.join(projectRoot, 'src');
const files = [];
console.log('分析目录:', srcDir);
console.log('正在收集文件信息...');
try {
// 遍历src目录
traverseDirectory(srcDir, files);
// 按文件大小降序排序
files.sort((a, b) => b.size - a.size);
// 计算总大小
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
console.log('\n=== 文件大小分析结果 ===');
console.log(`总文件数: ${files.length}`);
console.log(`总大小: ${formatFileSize(totalSize)}`);
console.log('\n体积最大的文件(前20个):');
console.log('------------------------------------------');
console.log('序号 | 相对路径 | 大小 | 占比');
console.log('------------------------------------------');
// 显示前20个最大的文件
const topFiles = files.slice(0, 20);
topFiles.forEach((file, index) => {
const relativePath = path.relative(projectRoot, file.path);
const percentage = ((file.size / totalSize) * 100).toFixed(2);
console.log(`${String(index + 1).padStart(3)} | ${relativePath.padEnd(50)} | ${file.formattedSize.padEnd(10)} | ${percentage}%`);
});
console.log('------------------------------------------');
// 按文件类型统计
console.log('\n按文件类型统计:');
const typeStats = {};
files.forEach(file => {
const ext = path.extname(file.path).toLowerCase() || '.noext';
if (!typeStats[ext]) {
typeStats[ext] = { count: 0, size: 0 };
}
typeStats[ext].count++;
typeStats[ext].size += file.size;
});
// 转换为数组并排序
const typeStatsArray = Object.entries(typeStats)
.map(([type, data]) => ({
type,
count: data.count,
size: data.size,
formattedSize: formatFileSize(data.size),
percentage: ((data.size / totalSize) * 100).toFixed(2)
}))
.sort((a, b) => b.size - a.size);
console.log('文件类型 | 文件数 | 总大小 | 占比');
console.log('------------------------------------------');
typeStatsArray.forEach(stat => {
console.log(`${stat.type.padEnd(10)} | ${String(stat.count).padStart(6)} | ${stat.formattedSize.padEnd(10)} | ${stat.percentage}%`);
});
} catch (error) {
console.error('分析文件大小时出错:', error.message);
process.exit(1);
}
}
// 执行分析
analyzeFileSizes();

当前分析结果如上,占比较大的还是一些资源文件。
优化无引用项目文件
分析项目中的无引用文件,将其删除。
const fs = require('fs');
const path = require('path');
const { promisify } = require('util');
// 颜色输出工具
const colors = {
reset: '\x1b[0m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
red: '\x1b[31m',
white: '\x1b[37m'
};
// 项目根目录
const projectRoot = path.resolve(process.cwd(), './');
const srcDir = path.join(projectRoot, 'src');
// 要分析的文件扩展名
const VALID_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.vue', '.css', '.scss', '.less'];
// 忽略的目录
const IGNORE_DIRS = [
'node_modules',
'dist',
'build',
'.git',
'.vscode',
'.cursor',
'public',
'analysis-duplication',
'_mock'
];
// 忽略的文件
const IGNORE_FILES = [
'vite-env.d.ts',
'main.tsx', // 入口文件通常不会被引用
'index.html',
'package.json',
'tsconfig.json',
'tailwind.config.ts',
'vite.config.ts',
'App.tsx' // 应用主组件
];
// 异步读取文件
const readFile = promisify(fs.readFile);
/**
* 递归获取目录下的所有文件
*/
async function getAllFiles(dir) {
let files = [];
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
// 忽略指定目录
if (entry.isDirectory() && IGNORE_DIRS.includes(entry.name)) {
continue;
}
if (entry.isDirectory()) {
const subFiles = await getAllFiles(fullPath);
files = [...files, ...subFiles];
} else if (entry.isFile()) {
// 只处理有效的文件扩展名
const ext = path.extname(entry.name);
if (VALID_EXTENSIONS.includes(ext) && !IGNORE_FILES.includes(entry.name)) {
files.push(fullPath);
}
}
}
return files;
}
/**
* 从文件路径获取模块名(用于匹配import语句)
*/
function getModuleName(filePath, projectRoot) {
const relativePath = path.relative(projectRoot, filePath);
// 移除扩展名
const nameWithoutExt = relativePath.replace(/\.[^/.]+$/, '');
// 转换为可导入的路径格式
return nameWithoutExt.replace(/\\/g, '/');
}
/**
* 解析文件中的导入语句
*/
function extractImports(content, filePath, projectRoot) {
const imports = [];
const relativePath = path.relative(projectRoot, filePath);
const dirPath = path.dirname(relativePath).replace(/\\/g, '/');
// 匹配ES模块导入
const esImportRegex = /import\s+(?:.*?)\s+from\s+['"](.+?)['"]/g;
let match;
while ((match = esImportRegex.exec(content)) !== null) {
imports.push(match[1]);
}
// 匹配require导入
const requireRegex = /require\(['"](.+?)['"]\)/g;
while ((match = requireRegex.exec(content)) !== null) {
imports.push(match[1]);
}
// 匹配动态导入
const dynamicImportRegex = /import\(['"](.+?)['"]\)/g;
while ((match = dynamicImportRegex.exec(content)) !== null) {
imports.push(match[1]);
}
return imports;
}
/**
* 检查文件是否被引用
*/
function isFileReferenced(filePath, allImports, projectRoot) {
const relativePath = path.relative(projectRoot, filePath);
const moduleName = getModuleName(filePath, projectRoot);
const fileName = path.basename(filePath);
const fileNameWithoutExt = fileName.replace(/\.[^/.]+$/, '');
const dirName = path.basename(path.dirname(filePath));
// 特殊处理组件和UI相关文件,它们可能通过配置或路由表引用
if (relativePath.includes('/components/') ||
relativePath.includes('/ui/') ||
relativePath.includes('/pages/') ||
relativePath.includes('/routes/')) {
// 这些文件可能通过路由配置、组件库配置等方式引用
// 检查是否有导入包含文件名或组件名
const componentLikeImports = allImports.some(importPath => {
// 检查是否包含文件名(可能是组件名)
return importPath.includes(fileNameWithoutExt) ||
importPath.includes(camelCaseToKebab(fileNameWithoutExt)) ||
importPath.includes(PascalCaseToKebab(fileNameWithoutExt));
});
if (componentLikeImports) {
return true;
}
}
// 检查是否有导入路径匹配此文件
return allImports.some(importPath => {
// 检查完整路径匹配
if (importPath === moduleName ||
importPath === moduleName + '.js' ||
importPath === moduleName + '.ts' ||
importPath === moduleName + '.jsx' ||
importPath === moduleName + '.tsx' ||
importPath === moduleName + '/index' ||
importPath === moduleName + '/index.js' ||
importPath === moduleName + '/index.ts' ||
importPath === moduleName + '/index.jsx' ||
importPath === moduleName + '/index.tsx') {
return true;
}
// 检查相对路径匹配
const possibleRelativePaths = [
'./' + fileName,
'./' + fileNameWithoutExt,
'../' + fileName,
'../' + fileNameWithoutExt,
'./' + fileNameWithoutExt + '/index',
'../' + fileNameWithoutExt + '/index',
'./' + dirName + '/' + fileNameWithoutExt,
'../' + dirName + '/' + fileNameWithoutExt
];
return possibleRelativePaths.some(relPath => {
// 精确匹配相对路径,避免部分匹配导致的误报
return importPath === relPath ||
importPath.startsWith(relPath + '/') ||
importPath === relPath + '.js' ||
importPath === relPath + '.ts' ||
importPath === relPath + '.jsx' ||
importPath === relPath + '.tsx';
});
});
}
/**
* 驼峰命名转短横线命名
*/
function camelCaseToKebab(str) {
return str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
}
/**
* 帕斯卡命名转短横线命名
*/
function PascalCaseToKebab(str) {
return str.charAt(0).toLowerCase() + str.slice(1).replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
}
/**
* 主函数
*/
async function main() {
try {
console.log(`${colors.cyan}=====================================${colors.reset}`);
console.log(`${colors.cyan}未引用文件分析工具${colors.reset}`);
console.log(`${colors.cyan}=====================================${colors.reset}`);
console.log(`${colors.yellow}开始扫描项目文件...${colors.reset}`);
const allFiles = await getAllFiles(srcDir);
console.log(`${colors.green}共扫描到 ${allFiles.length} 个文件${colors.reset}`);
console.log(`${colors.yellow}\n开始分析文件引用关系...${colors.reset}`);
const allImports = [];
const fileContents = new Map();
// 读取所有文件内容并提取导入
for (let i = 0; i < allFiles.length; i++) {
const filePath = allFiles[i];
try {
const content = await readFile(filePath, 'utf-8');
fileContents.set(filePath, content);
const imports = extractImports(content, filePath, projectRoot);
allImports.push(...imports);
// 显示进度
if ((i + 1) % 10 === 0 || i === allFiles.length - 1) {
console.log(`${colors.blue}进度: ${i + 1}/${allFiles.length} 个文件${colors.reset}`);
}
} catch (error) {
console.log(`${colors.red}读取文件失败: ${filePath} - ${error.message}${colors.reset}`);
}
}
console.log(`${colors.yellow}\n开始识别未引用文件...${colors.reset}`);
const unusedFiles = [];
for (const filePath of allFiles) {
if (!isFileReferenced(filePath, allImports, projectRoot)) {
unusedFiles.push(filePath);
}
}
console.log(`${colors.cyan}\n=====================================${colors.reset}`);
console.log(`${colors.cyan}分析结果${colors.reset}`);
console.log(`${colors.cyan}=====================================${colors.reset}`);
console.log(`${colors.blue}- 总文件数: ${colors.white}${allFiles.length}${colors.reset}`);
// 按目录分组未引用文件
const unusedFilesByDir = {};
unusedFiles.forEach(filePath => {
const dir = path.dirname(filePath);
const relativeDir = path.relative(projectRoot, dir);
if (!unusedFilesByDir[relativeDir]) {
unusedFilesByDir[relativeDir] = [];
}
unusedFilesByDir[relativeDir].push(filePath);
});
console.log(`${colors.blue}- 未引用文件数: ${colors.white}${unusedFiles.length}${colors.reset}`);
if (unusedFiles.length > 0) {
console.log(`${colors.yellow}\n未引用的文件列表(按目录分组):${colors.reset}`);
Object.entries(unusedFilesByDir).forEach(([dir, files]) => {
console.log(`${colors.cyan}\n${dir} (${files.length} 个文件):${colors.reset}`);
files.forEach((file, index) => {
const relativePath = path.relative(projectRoot, file);
console.log(`${colors.red} ${index + 1}. ${path.basename(file)}${colors.reset}`);
});
});
console.log(`${colors.yellow}\n未引用文件详情列表:${colors.reset}`);
unusedFiles.forEach((file, index) => {
const relativePath = path.relative(projectRoot, file);
console.log(`${colors.red}${index + 1}. ${relativePath}${colors.reset}`);
});
// 计算未引用文件占用的磁盘空间
let totalSize = 0;
for (const filePath of unusedFiles) {
try {
const stats = fs.statSync(filePath);
totalSize += stats.size;
} catch (error) {
// 忽略错误
}
}
console.log(`${colors.yellow}\n未引用文件总计大小: ${colors.white}${(totalSize / 1024).toFixed(2)} KB${colors.reset}`);
} else {
console.log(`${colors.green}\n恭喜!没有发现未引用的文件。${colors.reset}`);
}
console.log(`${colors.cyan}\n=====================================${colors.reset}`);
console.log(`${colors.cyan}分析完成${colors.reset}`);
console.log(`${colors.cyan}=====================================${colors.reset}`);
} catch (error) {
console.error(`${colors.red}分析过程中发生错误:${colors.white} ${error.message}${colors.reset}`);
console.error(error.stack);
process.exit(1);
}
}
// 执行主函数
main();
不建议写脚本直接删除,因为分析结果不准确性较大。此外可能有多个文件已废弃但互相引用的情况存在,这个分析结果只供参考,还是更多的需要人工判断。
优化未引用图片资源
分析未引用的图片和SVG文件,因为项目业务原因,排除一下资源目录。
- country-flag → src/assets/imges/countryimg(仅 .png)
- fingerprint-label → src/assets/svg/fingerprintLower(.svg 和 .png)
const fs = require('fs');
const path = require('path');
// 配置
const ASSETS_DIR = path.join(__dirname, '../src/assets');
const SRC_DIR = path.join(__dirname, '../src');
const EXCLUDED_DIRS = [
'src/assets/imges/countryimg',
'src/assets/svg/fingerprintLower'
];
// 需要扫描的代码文件扩展名
const CODE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.json', '.less', '.css', '.scss'];
// 图片文件扩展名
const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.ico'];
// 结果存储
const unusedImages = [];
const unusedSvgs = [];
const stats = {
totalImages: 0,
totalSvgs: 0,
ch
已在FreeBuf发表 0 篇文章
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf
客服小蜜蜂(微信:freebee1024)



