跳到主要内容

打包优化

问题

如何优化前端项目的打包体积和构建速度?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,在压缩阶段移除。

生效条件

  1. 使用 ESM(import/export)语法
  2. 生产模式构建
  3. 配置 sideEffects
  4. 使用支持的压缩工具
// ✅ 可 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 的区别?

答案

对比WebpackVite
开发启动打包后启动(慢)按需编译(快)
HMR重新打包模块ESM 原生 HMR
生产构建WebpackRollup
配置复杂度
生态成熟完善快速发展
适用场景复杂企业级现代项目

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
// - 公共代码提取
// - 按需加载

相关链接