打包优化
问题
如何优化前端项目的打包体积和构建速度?Tree Shaking、代码分割的原理是什么?
答案
打包优化是前端性能优化的重要环节,主要从减少体积、提升构建速度、优化加载三个方向进行。
优化目标
| 目标 | 指标 | 优化方向 |
|---|---|---|
| 减少体积 | JS < 200KB(gzip) | Tree Shaking、压缩、分包 |
| 提升构建速度 | 冷启动 < 10s | 缓存、并行、增量构建 |
| 优化加载 | 首屏 JS < 100KB | 代码分割、懒加载 |
Tree Shaking
Tree Shaking 是移除 JavaScript 中未使用代码的优化技术。
原理
生效条件
// ✅ ESM 语法,可 Tree Shaking
export function used() { }
export function unused() { } // 会被移除
// ❌ CommonJS 语法,无法 Tree Shaking
module.exports = {
used: function() { },
unused: function() { }
};
配置
// vite.config.ts
export default {
build: {
// 生产环境默认开启
minify: 'terser',
terserOptions: {
compress: {
dead_code: true,
drop_console: true,
drop_debugger: true
}
}
}
};
// webpack.config.js
module.exports = {
mode: 'production', // 自动开启 Tree Shaking
optimization: {
usedExports: true,
sideEffects: true
}
};
sideEffects 配置
// package.json
{
"name": "my-package",
"sideEffects": false, // 告诉打包工具所有模块无副作用
// 或指定有副作用的文件
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfills.js"
]
}
代码分割
入口分割
// webpack.config.js
module.exports = {
entry: {
main: './src/main.ts',
admin: './src/admin.ts'
},
output: {
filename: '[name].[contenthash].js'
}
};
动态导入
// React 懒加载
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
// Vue 路由懒加载
const routes = [
{
path: '/dashboard',
component: () => import('./pages/Dashboard.vue')
}
];
// 带注释的分包命名
const Chart = lazy(() => import(
/* webpackChunkName: "chart" */
/* webpackPrefetch: true */
'./components/Chart'
));
手动分包
// vite.config.ts
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
// React 相关
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
// UI 库
'ui-vendor': ['antd', '@ant-design/icons'],
// 工具库
'utils': ['lodash-es', 'dayjs', 'axios']
}
}
}
}
};
// 函数式分包(更灵活)
export default {
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('react')) return 'react-vendor';
if (id.includes('antd')) return 'ui-vendor';
return 'vendor';
}
}
}
}
}
};
Webpack SplitChunks
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
minSize: 20000,
maxSize: 250000,
cacheGroups: {
// React 核心
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react',
chunks: 'all',
priority: 40
},
// UI 组件库
antd: {
test: /[\\/]node_modules[\\/]antd[\\/]/,
name: 'antd',
chunks: 'all',
priority: 30
},
// 其他第三方库
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 20
},
// 公共模块
common: {
minChunks: 2,
name: 'common',
chunks: 'all',
priority: 10
}
}
}
}
};
压缩优化
JavaScript 压缩
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
build: {
minify: 'esbuild', // 默认,更快
// minify: 'terser', // 压缩率更高
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log']
},
mangle: {
safari10: true
}
}
}
});
CSS 压缩
// vite.config.ts
import cssnano from 'cssnano';
export default {
css: {
postcss: {
plugins: [
cssnano({
preset: ['default', {
discardComments: { removeAll: true },
normalizeWhitespace: true
}]
})
]
}
}
};
Gzip/Brotli 压缩
// vite-plugin-compression
import viteCompression from 'vite-plugin-compression';
export default {
plugins: [
// Gzip
viteCompression({
algorithm: 'gzip',
threshold: 10240 // 10KB 以上压缩
}),
// Brotli(压缩率更高)
viteCompression({
algorithm: 'brotliCompress'
})
]
};
依赖优化
分析依赖体积
# Webpack
npx webpack-bundle-analyzer stats.json
# Vite
npx vite-bundle-visualizer
替换大型依赖
// ❌ 整体引入 lodash(约 70KB)
import _ from 'lodash';
// ✅ 按需引入(约 2KB)
import debounce from 'lodash/debounce';
// ✅ 使用 lodash-es(支持 Tree Shaking)
import { debounce } from 'lodash-es';
// ✅ 使用更轻量的替代
// moment.js (~300KB) → dayjs (~2KB)
// lodash → 原生方法或 radash
按需加载 UI 库
// Ant Design 按需加载
// vite.config.ts
import Components from 'unplugin-vue-components/vite';
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
export default {
plugins: [
Components({
resolvers: [AntDesignVueResolver()]
})
]
};
externals 外部化
// webpack.config.js
module.exports = {
externals: {
react: 'React',
'react-dom': 'ReactDOM',
echarts: 'echarts'
}
};
<!-- 使用 CDN 加载 -->
<script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js"></script>
构建速度优化
Vite 优化
// vite.config.ts
export default {
// 预构建依赖
optimizeDeps: {
include: ['lodash-es', 'axios'],
exclude: ['some-local-package']
},
// esbuild 配置
esbuild: {
target: 'esnext',
drop: ['console', 'debugger']
},
// 构建缓存
build: {
// 启用 CSS 代码分割
cssCodeSplit: true,
// 构建目标
target: 'esnext'
}
};
Webpack 优化
// webpack.config.js
const path = require('path');
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
// 缓存
cache: {
type: 'filesystem',
cacheDirectory: path.resolve(__dirname, '.cache')
},
// 限制解析范围
resolve: {
modules: [path.resolve(__dirname, 'src'), 'node_modules'],
extensions: ['.ts', '.tsx', '.js'],
alias: {
'@': path.resolve(__dirname, 'src')
}
},
// 多线程压缩
optimization: {
minimizer: [
new TerserPlugin({
parallel: true,
terserOptions: {
compress: { drop_console: true }
}
})
]
},
// 排除不需要处理的模块
module: {
noParse: /jquery|lodash/,
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: 'ts-loader'
}
]
}
};
使用 SWC/esbuild
// 使用 SWC 替代 Babel(速度提升 20-70x)
// .swcrc
{
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": true
},
"transform": {
"react": {
"runtime": "automatic"
}
}
}
}
// webpack with swc-loader
module.exports = {
module: {
rules: [
{
test: /\.tsx?$/,
use: 'swc-loader',
exclude: /node_modules/
}
]
}
};
缓存策略
文件名哈希
// vite.config.ts
export default {
build: {
rollupOptions: {
output: {
// 入口文件
entryFileNames: 'js/[name].[hash].js',
// 代码分割产物
chunkFileNames: 'js/[name].[hash].js',
// 静态资源
assetFileNames: 'assets/[name].[hash][extname]'
}
}
}
};
内容哈希 vs 块哈希
// webpack.config.js
module.exports = {
output: {
// contenthash: 基于文件内容,推荐
filename: '[name].[contenthash:8].js',
// chunkhash: 基于 chunk 内容
// hash: 基于整体构建,不推荐
}
};
产物分析
// 分析打包体积
// package.json
{
"scripts": {
"analyze": "vite build && npx vite-bundle-visualizer",
"analyze:webpack": "webpack --profile --json > stats.json && npx webpack-bundle-analyzer stats.json"
}
}
常见面试问题
Q1: Tree Shaking 的原理和条件?
答案:
原理:基于 ESM 的静态分析,标记未使用的 export,在压缩阶段移除。
生效条件:
- 使用 ESM(import/export)语法
- 生产模式构建
- 配置 sideEffects
- 使用支持的压缩工具
// ✅ 可 Tree Shaking
import { debounce } from 'lodash-es';
// ❌ 无法 Tree Shaking
const _ = require('lodash');
import _ from 'lodash';
Q2: 如何分析和优化打包体积?
答案:
// 1. 分析
// npx vite-bundle-visualizer
// npx webpack-bundle-analyzer
// 2. 常见优化
const optimizations = {
// 代码分割
splitChunks: '按路由/功能分割',
// 按需加载
lazyLoad: '动态 import',
// 依赖优化
dependencies: {
'lodash → lodash-es': '支持 Tree Shaking',
'moment → dayjs': '更小体积',
'antd': '按需加载组件'
},
// 外部化
externals: 'React/Vue 等用 CDN',
// 压缩
compress: 'Gzip/Brotli'
};
Q3: Webpack 和 Vite 的区别?
答案:
| 对比 | Webpack | Vite |
|---|---|---|
| 开发启动 | 打包后启动(慢) | 按需编译(快) |
| HMR | 重新打包模块 | ESM 原生 HMR |
| 生产构建 | Webpack | Rollup |
| 配置复杂度 | 高 | 低 |
| 生态 | 成熟完善 | 快速发展 |
| 适用场景 | 复杂企业级 | 现代项目 |
Q4: 如何提升构建速度?
答案:
const buildOptimization = {
// 1. 开启缓存
cache: 'filesystem 缓存',
// 2. 使用更快的工具
tools: 'esbuild/SWC 替代 Babel/Terser',
// 3. 减少解析范围
exclude: 'node_modules 排除',
// 4. 并行处理
parallel: 'thread-loader/TerserPlugin parallel',
// 5. 预构建
preBuild: 'Vite optimizeDeps'
};
Q5: 代码分割的策略?
答案:
// 1. 路由级分割
const Home = lazy(() => import('./pages/Home'));
// 2. 组件级分割
const HeavyChart = lazy(() => import('./components/Chart'));
// 3. 第三方库分割
manualChunks: {
'react-vendor': ['react', 'react-dom'],
'ui-vendor': ['antd']
}
// 4. 公共模块提取
splitChunks: {
cacheGroups: {
common: { minChunks: 2 }
}
}
// 分割原则
// - 首屏代码 < 100KB
// - 单个 chunk < 250KB
// - 公共代码提取
// - 按需加载