构建优化实践(Vite + Webpack)
1. 构建工具选型对比
1.1 主流构建工具概览
| 工具 | 语言 | 底层 | 开发模式 | 生产模式 | 生态 |
|---|---|---|---|---|---|
| Vite | TypeScript | esbuild (预构建) + Rollup (打包) | 原生 ESM 按需编译 | Rollup 打包 + 插件 | 丰富,兼容 Rollup 插件 |
| Webpack | JavaScript | JavaScript + Node.js | 全量打包 + HMR | 全量打包 + 优化 | 最丰富,成熟稳定 |
| Turbopack | Rust | Next.js 定制 | 增量计算 + 函数级缓存 | 开发模式专用 | 仅 Next.js 生态 |
| Rspack | Rust | 自研(兼容 Webpack 插件) | Rust 原生并行编译 | Rust 原生并行打包 | 兼容 Webpack loader/plugin |
1.2 开发体验对比(HMR 速度)
| 场景 | Vite | Webpack 5 | Rspack | Turbopack |
|---|---|---|---|---|
| 冷启动(1000 模块) | ~300ms(esbuild 预构建) | ~10s+ | ~500ms | ~200ms |
| 热更新(单文件改动) | <50ms(ESM 按需更新) | ~200ms-1s(重建 chunk) | <100ms | <50ms |
| 热更新(大文件编辑) | 几乎不受影响 | 随模块数线性增长 | 受轻微影响 | 几乎不受影响 |
| 首次页面加载 | 需要额外 ESM 请求 | 直接加载打包后文件 | 直接加载 | 分段加载 |
// Vite 开发模式原理:利用浏览器原生 ESM,无需打包
// 源文件 → esbuild 转译 → 浏览器直接 import
// 修改文件 → 仅发送被修改的模块 → 浏览器自行更新依赖图
// Webpack 开发模式原理:打包为 bundle 后推送
// 源文件 → 全量打包 → bundle 推送到内存 → WebSocket 通知更新
// 修改文件 → 重新打包该 module → 发送增量 patch → eval 执行
1.3 生产构建速度对比
| 指标 | Vite (Rollup) | Webpack 5 | Rspack | Turbopack |
|---|---|---|---|---|
| 中型项目(~500 模块) | ~8s | ~25s | ~5s | 仅开发模式 |
| 大型项目(~3000 模块) | ~30s | ~90s+ | ~15s | 仅开发模式 |
| 产物体积优化 | 优秀(Rollup 天然 Tree-shaking) | 良好(需配置) | 良好 | N/A |
| 代码分割能力 | 灵活(manualChunks) | 丰富(SplitChunks) | 兼容 Webpack | N/A |
1.4 选型建议
| 项目类型 | 推荐工具 | 理由 |
|---|---|---|
| 新项目(Vue/React) | Vite | 开箱即用,HMR 极快,配置简单 |
| 遗留 Webpack 项目迁移 | Rspack | 兼容 Webpack 配置,迁移成本低,构建快 5-10x |
| Next.js 项目 | Turbopack | 框架内置,开发模式优化 |
| 需要 Webpack 特有插件/loader | Webpack 5 | 生态最成熟,兼容性最好 |
| Monorepo 大型项目 | Vite + Rspack | Vite 写配置,Rspack 做生产构建 |
2. Vite 构建优化
2.1 预构建依赖优化(optimizeDeps)
原理:Vite 在开发模式下使用 esbuild 对 node_modules 中的依赖进行预构建,将 CommonJS/UMD 模块转为 ESM,并合并分散的依赖文件以减少 HTTP 请求数。
配置示例:
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
optimizeDeps: {
// 明确指定需要预构建的依赖(默认自动检测)
include: [
'lodash-es',
'dayjs',
'axios',
],
// 排除不需要预构建的依赖(已为 ESM 的库)
exclude: [
'@vueuse/core',
'pinia',
],
// 当依赖项有动态导入或 require 时,强制预构建
force: true, // 开发中调试依赖时使用
},
})
注意事项/踩坑点:
1. 某些 ESM 包如果包含动态 import,需要手工加入 include
2. 修改 optimizeDeps 后需重启 dev server 才能生效
3. force: true 会清除缓存,不要在生产构建中使用
4. 遇到 "Outdated optimisticDeps" 警告时,清空 node_modules/.vite
✅ 好的实践:明确声明项目直接依赖
// vite.config.ts
export default defineConfig({
optimizeDeps: {
include: [
// 将分散的模块合并为一个请求
'echarts/core',
'echarts/charts',
'echarts/components',
],
},
})
❌ 不好的实践:不合理的 exclude
// ❌ 误将包含 require() 的 CJS 库排除
export default defineConfig({
optimizeDeps: {
exclude: ['lodash'], // lodash 是 CJS,排除后浏览器无法运行
},
})
2.2 代码分割(Code Splitting)
原理:将代码拆分为多个 chunk,按需加载,减少首屏加载体积。
路由懒加载
Vue 3 实现(defineAsyncComponent + Vue Router):
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { defineAsyncComponent } from 'vue'
import { ElLoading } from 'element-plus'
// 方式一:使用 defineAsyncComponent(推荐)
const routes = [
{
path: '/dashboard',
name: 'Dashboard',
component: defineAsyncComponent({
loader: () => import('@/views/Dashboard/index.vue'),
loadingComponent: () => <ElLoading />, // 加载中组件
errorComponent: () => <div>加载失败</div>, // 错误组件
delay: 200, // 延迟显示 loading(毫秒)
timeout: 10000, // 超时时间
}),
},
{
path: '/users',
name: 'Users',
// 方式二:直接使用动态 import(Vite 自动分割)
component: () => import('@/views/Users/index.vue'),
},
]
const router = createRouter({
history: createWebHistory(),
routes,
})
export default router
React 实现(lazy + Suspense):
// src/router/index.tsx
import { lazy, Suspense } from 'react'
import { createBrowserRouter } from 'react-router-dom'
import { Spin } from 'antd'
// 路由懒加载组件
const Dashboard = lazy(() => import('@/views/Dashboard'))
const Users = lazy(() => import('@/views/Users'))
// 加载中组件
const PageLoading = () => (
<div style={{ display: 'flex', justifyContent: 'center', padding: '48px 0' }}>
<Spin size="large" />
</div>
)
const router = createBrowserRouter([
{
path: '/dashboard',
element: (
<Suspense fallback={<PageLoading />}>
<Dashboard />
</Suspense>
),
},
{
path: '/users',
element: (
<Suspense fallback={<PageLoading />}>
<Users />
</Suspense>
),
},
])
export default router
2.3 manualChunks 分包策略
原理:在 Vite 的生产构建阶段(Rollup),通过 build.rollupOptions.output.manualChunks 自定义 chunk 分割规则。
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import react from '@vitejs/plugin-react'
export default defineConfig({
build: {
rollupOptions: {
output: {
// 分包策略
manualChunks: {
// 1. Vue 生态全家桶
'vue-vendor': [
'vue',
'vue-router',
'pinia',
'vue-i18n',
],
// 2. React 生态全家桶
'react-vendor': [
'react',
'react-dom',
'react-router-dom',
'zustand',
],
// 3. UI 组件库(单独分包,避免与业务代码混装)
'ui-element': ['element-plus'],
'ui-antd': ['antd', '@ant-design/icons'],
'ui-vxe': ['vxe-table', 'vxe-pc-ui'],
// 4. 工具库
'utils-vendor': [
'lodash-es',
'dayjs',
'axios',
'zxcvbn',
],
// 5. 图表库
'chart-vendor': ['echarts', 'vue-echarts'],
},
},
},
// CSS 提取配置
cssCodeSplit: false, // 合并所有 CSS 为一个文件
},
})
| 分包策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| vendor 分包 | 第三方库变更少,缓存利用率高 | 首次加载 vendor 体积大 | 所有项目 |
| 按功能分包 | 按业务模块拆分,按需加载 | 模块间依赖复杂时可能重复 | 大型项目 |
| 按 UI 库分包 | Element Plus / Ant Design 独立 | 多个 UI 库时增多请求数 | 多 UI 库项目 |
| 按页面分包 | 结合路由懒加载,首屏最优 | 公共依赖可能重复打包 | 页面级应用 |
✅ 更好的分包策略:动态检测:
// vite.config.ts
import { defineConfig } from 'vite'
// 自动检测 node_modules 中的依赖大小,按规则分包
function createManualChunks() {
const vendorGroups: Record<string, RegExp[]> = {
'vue-vendor': [/node_modules\/vue/, /node_modules\/@vue/],
'react-vendor': [/node_modules\/react/],
'ui-element': [/node_modules\/element-plus/, /node_modules\/@element-plus/],
'ui-antd': [/node_modules\/antd/, /node_modules\/@ant-design/],
'utils': [/node_modules\/lodash/, /node_modules\/dayjs/, /node_modules\/axios/],
'echarts': [/node_modules\/echarts/, /node_modules\/zrender/],
}
return (id: string) => {
for (const [name, patterns] of Object.entries(vendorGroups)) {
if (patterns.some((p) => p.test(id))) {
return name
}
}
// 其他 node_modules 归入 vendor
if (id.includes('node_modules')) {
return 'vendor'
}
}
}
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: createManualChunks(),
},
},
},
})
❌ 不好的实践:
// ❌ 未配置 manualChunks,所有代码打包到同一个 JS 文件
export default defineConfig({
build: {
rollupOptions: {
output: {
// 没有 manualChunks 配置 → 单文件超大 bundle
},
},
},
})
// ❌ 过度分包导致请求数爆炸
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
// 每个 node_modules 包都独立为一个 chunk
if (id.includes('node_modules')) {
return id.split('/node_modules/')[1].split('/')[0]
}
},
},
},
},
})
2.4 CSS 提取与压缩
配置示例:
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
build: {
// CSS 代码分割(默认 true,每个路由一个 CSS 文件)
cssCodeSplit: true,
// CSS 压缩
cssMinify: 'esbuild', // 'esbuild' | 'lightningcss' | boolean
// 生成 sourcemap
sourcemap: false,
// 目标浏览器
target: 'es2015',
},
// CSS 预处理配置
css: {
preprocessorOptions: {
scss: {
// 注入全局变量和 mixin,避免每个文件手动 @import
additionalData: `@use "@/styles/variables" as *;`,
api: 'modern-compiler', // 使用 Dart Sass
},
},
// CSS modules 配置
modules: {
localsConvention: 'camelCaseOnly',
},
// PostCSS 配置(自动添加浏览器前缀等)
postcss: {
plugins: [
require('autoprefixer')({
overrideBrowserslist: ['last 2 versions', '> 1%', 'not dead'],
}),
],
},
},
})
| 配置项 | 取值 | 说明 |
|---|---|---|
cssMinify |
'esbuild' |
默认,esbuild 快速压缩 |
cssMinify |
'lightningcss' |
更快,支持更现代化的 CSS 语法 |
cssMinify |
false |
不压缩,调试用 |
cssCodeSplit |
true |
开启 CSS 代码分割,按 async chunk 拆分 |
2.5 静态资源处理
原理:Vite 在构建时会自动处理静态资源,小于 assetsInlineLimit 的文件会被内联为 Base64,减少 HTTP 请求。
// vite.config.ts
import { defineConfig } from 'vite'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
export default defineConfig({
build: {
// 静态资源输出目录
assetsDir: 'assets',
// 内联阈值(字节),小于此值的资源转为 Base64
assetsInlineLimit: 4096, // 4KB
},
plugins: [
// 图片压缩插件(生产构建时自动压缩图片)
ViteImageOptimizer({
png: { quality: 80 },
jpeg: { quality: 80 },
webp: { quality: 75 },
avif: { quality: 60 },
}),
],
})
| 资源类型 | Vite 处理方式 | 优化建议 |
|---|---|---|
| 图片(png/jpg/gif/svg) | 小于阈值内联,否则复制到 assetsDir | 使用 WebP/AVIF 格式,图片压缩 |
| 字体(woff/woff2) | 复制到 assetsDir | 仅保留需要的字重,使用 font-display: swap |
| 视频 | 复制到 assetsDir | 使用 CDN 托管,避免打包 |
| JSON/YAML | 转为 ES module | 按需 import,避免整体加载 |
2.6 Gzip / Brotli 压缩
原理:在构建时生成 .gz / .br 文件,配合 Nginx 等服务器在传输时启用压缩,减少网络传输体积。
// vite.config.ts
import { defineConfig } from 'vite'
import viteCompression from 'vite-plugin-compression'
export default defineConfig({
plugins: [
viteCompression({
// 压缩算法
algorithm: 'brotliCompress', // 'gzip' | 'brotliCompress' | 'deflate'
// 匹配的文件类型
ext: '.br', // gzip 对应 '.gz'
// 只压缩超过此大小的文件(字节)
threshold: 1024, // 1KB
// 删除源文件,只保留压缩文件(需服务器配置 Content-Encoding)
deleteOriginFile: false,
// 压缩质量(brotli: 0-11, gzip: 0-9)
compressionOptions: {
level: 9,
},
}),
],
})
# Nginx 配置示例
server {
# 启用 Brotli 压缩(需安装 brotli 模块)
brotli on;
brotli_types text/plain text/css application/json application/javascript;
brotli_comp_level 6;
# 启用 Gzip 压缩(兜底)
gzip on;
gzip_types text/plain text/css application/json application/javascript;
gzip_comp_level 6;
gzip_min_length 1000;
}
2.7 Tree Shaking 原理与保障
原理:Tree Shaking 通过静态分析 ES Module 的 import/export 语法,移除未被引用的代码。
// Vite 的 Tree Shaking 由 Rollup 在生产构建时完成
// 需要满足的条件:
// ✅ 可 Tree Shaking - 使用 ESM 语法
import { debounce } from 'lodash-es' // 只引入 debounce 函数
import { ref, computed } from 'vue' // 按需引入 Vue API
// ❌ 不可 Tree Shaking - CommonJS 语法
const _ = require('lodash') // 全量引入,无法 tree-shake
import * as lodash from 'lodash' // 全量引入
| 条件 | 说明 |
|---|---|
| ES Module | 必须使用 import/export 语法 |
| sideEffects | package.json 中声明 "sideEffects": false |
| 纯函数调用 | 顶层调用的副作用函数不会被移除 |
| 动态引入 | import() 按需加载天然支持 |
✅ 保障 Tree Shaking 的配置:
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
build: {
rollupOptions: {
// 明确告诉 Rollup 哪些模块无副作用
treeshake: {
moduleSideEffects: (id) => {
// 排除 CSS 和 CSS modules
if (id.endsWith('.css') || id.endsWith('.module.css')) {
return true
}
// polyfill 保留副作用
if (id.includes('core-js')) {
return true
}
return false
},
// 预设属性值:'smallest' | 'safest' | 'recommended'
preset: 'recommended',
},
},
},
})
注意事项/踩坑点:
1. lodash(CJS)无法 Tree Shaking,必须使用 lodash-es
2. 某些 UI 库的全局 CSS 引入会阻止 Tree Shaking
3. sideEffects: false 声明不当可能导致样式丢失
4. Babel 转译时可能破坏 ESM 语法,使用 @babel/preset-env 的 modules: false
3. Webpack 构建优化(对比 / 迁移场景)
3.1 优化 Loader 配置
// webpack.config.js(或 webpack.config.ts)
const path = require('path')
module.exports = {
module: {
rules: [
{
test: /\.(ts|tsx)$/,
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true, // 启用 babel 缓存
},
},
],
// ✅ 限制处理范围
include: [path.resolve(__dirname, 'src')],
exclude: [/node_modules/, /dist/],
},
{
test: /\.svg$/,
// ✅ 使用资源模块替代 url-loader/file-loader
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 4 * 1024, // 4KB
},
},
},
],
},
}
| 优化点 | 说明 | 效果 |
|---|---|---|
include |
限定 loader 只处理 src 目录 | 减少文件扫描范围 |
exclude |
排除 node_modules | 避免重复处理第三方库 |
cacheDirectory |
开启缓存 | 增量编译提速 2-5x |
type: 'asset' |
Webpack 5 内置资源模块 | 替代 url-loader/file-loader |
3.2 thread-loader 多线程
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(ts|tsx)$/,
use: [
'thread-loader', // 多线程(放在最前面)
{
loader: 'babel-loader',
options: {
cacheDirectory: true,
},
},
],
include: [path.resolve(__dirname, 'src')],
},
],
},
}
| 配置项 | 说明 | 建议值 |
|---|---|---|
workers |
线程数 | CPU 核心数 - 1(默认) |
workerParallelJobs |
每个线程的任务数 | 20(默认) |
poolTimeout |
线程池闲置超时 | 2000ms(默认) |
注意事项:
1. thread-loader 有线程启动开销,小型项目可能反而更慢
2. 不能与 mini-css-extract-plugin 配合使用
3. 在 Vue SFC 中慎用,可能引起编译问题
4. 仅对 CPU 密集型 loader(babel-loader, ts-loader)有效
3.3 cache 缓存
// webpack.config.js
module.exports = {
// Webpack 5 内置缓存
cache: {
type: 'filesystem', // 'memory' | 'filesystem'
cacheDirectory: path.resolve(__dirname, '.temp_cache'),
buildDependencies: {
// 配置文件变了则清除缓存
config: [__filename],
},
// 缓存版本,升级依赖后需手动更新
version: '1.0',
// 缓存内存存储限制(filesystem 也经过内存)
maxMemoryGenerations: 1,
// 按模块类型缓存
profile: true,
},
}
| 缓存类型 | 优点 | 缺点 |
|---|---|---|
memory |
无磁盘 I/O,快速 | 进程退出后丢失 |
filesystem |
跨进程共享,持久化 | 首次构建需写入缓存 |
3.4 SplitChunksPlugin 分包
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all', // 'initial' | 'async' | 'all'
// 包的最小体积(字节),小于此值不拆分
minSize: 20000, // 20KB
// 包的最大体积,超过会进一步拆分
maxSize: 244000, // 约 240KB
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
cacheGroups: {
// 1. Vue 全家桶
vue: {
test: /[\\/]node_modules[\\/](vue|vue-router|pinia|vue-i18n)[\\/]/,
name: 'vendor-vue',
priority: 20,
},
// 2. React 全家桶
react: {
test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom|zustand)[\\/]/,
name: 'vendor-react',
priority: 20,
},
// 3. UI 库
ui: {
test: /[\\/]node_modules[\\/](element-plus|antd|@ant-design|vxe-table)[\\/]/,
name: 'vendor-ui',
priority: 15,
},
// 4. 第三方库
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
priority: 10,
},
// 5. 公共代码(被多个 chunk 引用)
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
},
},
},
},
}
3.5 Webpack 迁移到 Vite 对比
| Webpack 配置 | Vite 等效配置 | 说明 |
|---|---|---|
babel-loader |
内置 esbuild | Vite 使用 esbuild 转译,速度 10-20x |
ts-loader |
内置 esbuild | 仅转译,不做类型检查 |
url-loader/file-loader |
assetsInlineLimit |
Vite 内置资源处理 |
css-loader/style-loader |
内置 CSS 处理 | Vite 天然支持 CSS |
MiniCssExtractPlugin |
build.cssCodeSplit |
Vite 默认提取 CSS |
TerserPlugin |
build.minify: 'esbuild' |
默认使用 esbuild 压缩 |
SplitChunksPlugin |
manualChunks |
等效功能 |
DefinePlugin |
define |
语法略有不同 |
4. 包体积分析
4.1 rollup-plugin-visualizer(Vite)
// vite.config.ts
import { defineConfig } from 'vite'
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
visualizer({
// 输出分析报告
filename: 'dist/stats.html',
// 图表模板
template: 'treemap', // 'treemap' | 'sunburst' | 'network'
// 打开分析页面
open: true,
// 显示 gzip 后大小
gzipSize: true,
// 显示 brotli 后大小
brotliSize: true,
}),
],
})
4.2 webpack-bundle-analyzer(Webpack)
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'dist/stats.html',
openAnalyzer: true,
generateStatsFile: true,
statsFilename: 'dist/stats.json',
}),
],
}
4.3 如何分析并优化大依赖
| 步骤 | 方法 | 说明 |
|---|---|---|
| 1. 生成分析报告 | visualizer / bundle-analyzer | 可视化查看各模块大小 |
| 2. 定位大依赖 | 按大小排序,关注 >100KB 的模块 | 查找意外引入的依赖 |
| 3. 检查重复打包 | 分析是否有多个版本的同个库 | 使用 resolve.alias 统一版本 |
| 4. 评估必要性 | 是否只有部分功能被使用 | 考虑按需导入或替换 |
| 5. 制定优化方案 | 替换、按需、分包 | 针对性处理 |
✅ 分析优化实战:
# 1. 构建并生成分析报告
pnpm build
# 2. 查看 dist/stats.html
# 发现 moment.js 贡献了 600KB+,但只用了 format() 和 locale()
// 3. 制定优化方案
// ❌ 优化前:moment.js 全量打包(~600KB)
import moment from 'moment'
moment.locale('zh-cn')
console.log(moment().format('YYYY-MM-DD'))
// ✅ 优化后:替换为 dayjs(~6KB)
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'
dayjs.locale('zh-cn')
console.log(dayjs().format('YYYY-MM-DD'))
5. 构建产物优化
5.1 路由懒加载(已在上文详述)
5.2 组件级懒加载
Vue 3 实现:
<script setup lang="ts">
import { defineAsyncComponent, ref } from 'vue'
// 组件级懒加载:仅在需要时加载
const HeavyChart = defineAsyncComponent(() => import('@/components/HeavyChart.vue'))
// 条件渲染时加载(弹框、Tab 切换)
const showChart = ref(false)
// 预加载策略:hover 时提前加载
function preloadChart() {
const preloader = import('@/components/HeavyChart.vue')
}
</script>
<template>
<div>
<el-button @click="showChart = true" @mouseenter="preloadChart">
显示图表
</el-button>
<!-- 懒加载组件仅在 v-if 为 true 时才开始加载 -->
<HeavyChart v-if="showChart" />
</div>
</template>
React 实现:
import { useState, lazy, Suspense } from 'react'
// 组件级懒加载
const HeavyChart = lazy(() => import('@/components/HeavyChart'))
function Dashboard() {
const [showChart, setShowChart] = useState(false)
// 预加载策略
const preloadChart = () => {
import('@/components/HeavyChart')
}
return (
<div>
<button
onClick={() => setShowChart(true)}
onMouseEnter={preloadChart}
>
显示图表
</button>
{showChart && (
<Suspense fallback={<div>加载中...</div>}>
<HeavyChart />
</Suspense>
)}
</div>
)
}
5.3 第三方库按需导入
Element Plus(Vue 3)
// vite.config.ts
import { defineConfig } from 'vite'
import ElementPlus from 'unplugin-element-plus/vite'
export default defineConfig({
plugins: [
ElementPlus({
// 配置按需导入
useSource: true,
// 图标按需导入
icon: 'ElIcon',
}),
],
})
// 使用方式(自动按需导入,无需手动 import)
// ❌ 不需要这样
// import { ElButton } from 'element-plus'
// ✅ 直接使用,插件自动按需导入
<template>
<el-button type="primary">提交</el-button>
</template>
Ant Design(React)
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
// Ant Design v5 已默认支持 Tree Shaking
// 只需确保使用 ESM 导入即可
})
// ✅ Ant Design v5 按需使用(默认 Tree Shaking)
import { Button, Table, Modal } from 'antd'
function UserList() {
return (
<Table
dataSource={users}
columns={columns}
/>
)
}
5.4 moment.js → dayjs / date-fns
| 对比项 | moment.js | dayjs | date-fns |
|---|---|---|---|
| 体积(min+gzip) | ~70KB | ~7KB | ~10KB(按需) |
| API 兼容性 | 标准 | 兼容 moment API | 函数式 API |
| Tree Shaking | 不支持 | 支持(插件化) | 支持(按函数导入) |
| 国际化 | 内置 | 按需加载 | 按需加载 |
| 不可变性 | mutable(有坑) | immutable | immutable |
✅ 迁移示例:dayjs:
// ❌ 优化前:moment.js
import moment from 'moment'
import 'moment/locale/zh-cn'
moment.locale('zh-cn')
const date = moment('2024-01-15').format('YYYY-MM-DD')
const diff = moment('2024-01-20').diff(moment('2024-01-15'), 'day')
const isValid = moment('2024-01-15').isValid()
// ✅ 优化后:dayjs(体积缩小 90%)
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'
import relativeTime from 'dayjs/plugin/relativeTime'
import isBetween from 'dayjs/plugin/isBetween'
dayjs.locale('zh-cn')
dayjs.extend(relativeTime)
dayjs.extend(isBetween)
const date = dayjs('2024-01-15').format('YYYY-MM-DD')
const diff = dayjs('2024-01-20').diff(dayjs('2024-01-15'), 'day')
const isValid = dayjs('2024-01-15').isValid()
5.5 lodash → lodash-es
| 对比项 | lodash | lodash-es |
|---|---|---|
| 模块系统 | CommonJS | ES Module |
| Tree Shaking | 不支持 | 天然支持 |
| 体积(全量) | ~540KB | ~400KB(可 Tree Shaking) |
| 按需导入 | 每个函数独立包 | ESM 按需导入 |
✅ 转换示例:
// ❌ 优化前:lodash(全量导入,不可 Tree Shaking)
import _ from 'lodash'
_.debounce(fn, 300)
_.throttle(fn, 1000)
_.cloneDeep(data)
// ✅ 优化后:lodash-es(按需导入,Tree Shaking)
import { debounce, throttle, cloneDeep } from 'lodash-es'
debounce(fn, 300)
throttle(fn, 1000)
cloneDeep(data)
6. CDN 策略
6.1 externals 排除第三方库
原理:构建时排除指定的第三方库,不打包到 bundle 中,改由外部 CDN 在运行时提供。
Vite 配置
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
build: {
rollupOptions: {
external: [
'vue',
'vue-router',
'pinia',
'element-plus',
'echarts',
],
output: {
// 为外部库声明全局变量名(供 CDN 引入的 UMD 包使用)
globals: {
vue: 'Vue',
'vue-router': 'VueRouter',
pinia: 'Pinia',
'element-plus': 'ElementPlus',
echarts: 'echarts',
},
},
},
},
})
Webpack 配置
// webpack.config.js
module.exports = {
externals: {
vue: 'Vue',
'vue-router': 'VueRouter',
pinia: 'Pinia',
'element-plus': 'ElementPlus',
echarts: 'echarts',
},
}
HTML 模板引入 CDN
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>应用</title>
<!-- 使用 Vite 插件自动注入 CDN 链接 -->
<!-- CDN 引入 Vue 生态 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.4/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-router@4/dist/vue-router.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pinia@2/dist/pinia.iife.prod.js"></script>
<!-- CDN 引入 UI 库 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-plus/dist/index.min.css" />
<script src="https://cdn.jsdelivr.net/npm/element-plus/dist/index.full.min.js"></script>
<!-- CDN 引入 ECharts -->
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
6.2 CDN 插件自动化
// vite.config.ts
import { defineConfig } from 'vite'
import { cdn } from 'vite-plugin-cdn2'
export default defineConfig({
plugins: [
cdn({
// 自动从 CDN 加载,并配置 externals
modules: [
'vue',
'vue-router',
'pinia',
'element-plus',
'echarts',
],
// CDN 源
prodUrl: 'https://cdn.jsdelivr.net/npm/{name}@{version}/{path}',
}),
],
})
6.3 CDN vs 打包对比
| 对比项 | 打包到 bundle | CDN 外部引用 |
|---|---|---|
| 首次加载 | 加载完整 bundle | 并发加载多个 CDN 文件 |
| 缓存 | 应用更新后缓存失效 | CDN 缓存独立,库更新频率低时缓存率高 |
| 可靠性 | 自控,无外部依赖 | 依赖 CDN 可用性(兜底:fallback 到本地) |
| 请求数 | 1 个 JS 请求(或少量 chunk) | 多个 CDN 请求 |
| 构建速度 | 需要处理所有依赖 | 排除外部依赖,构建更快 |
✅ 推荐策略:混合使用
中小型项目:全部打包,简单可靠
大型项目:稳定的大依赖走 CDN(Vue, React, ECharts),业务代码打包
极端优化场景:关键依赖走 CDN,使用 preconnect + prefetch 预热连接
7. 构建性能检查清单
开发环境
□ 是否使用 Vite / Rspack 替代 Webpack(开发构建提速 5-10x)
□ 是否配置 optimizeDeps.include 处理动态 import 依赖
□ 是否使用 esbuild 替代 Babel(Vite 默认,无需配置)
□ 是否使用 @vitejs/plugin-vue-jsx 替代 Babel 转 JSX
□ 是否开启 Webpack 的 cache.type = 'filesystem'
□ 是否使用 thread-loader 处理 CPU 密集型 loader(仅大型 Webpack 项目)
□ 是否使用 HMR 替代全量刷新
生产环境
□ 是否启用路由懒加载(Vue: defineAsyncComponent / React: lazy + Suspense)
□ 是否配置 manualChunks / SplitChunks 分包策略
□ 是否将 UI 库、工具库独立分包
□ 是否使用 lodash-es 替代 lodash
□ 是否使用 dayjs 替代 moment.js
□ 是否使用 antd v5+ / element-plus(天然支持 Tree Shaking)
□ 是否配置 CSS 提取和压缩(cssMinify: 'esbuild' 或 'lightningcss')
□ 是否启用 Gzip / Brotli 压缩
□ 是否处理图片压缩(WebP / AVIF 格式转换)
□ 是否使用 esbuild 或 swc 作为 minifier
□ 是否移除 console.log(esbuild: drop: ['console'])
□ 是否关闭 sourcemap(sourcemap: false)
构建产物分析
□ 是否运行 visualizer / bundle-analyzer 查看包体积
□ 是否检查是否有重复打包的依赖(多版本冲突)
□ 是否检查是否有意外打包的大依赖
□ 是否检查是否有未使用的 polyfill
□ 是否检查 CDN 引入的库版本是否与开发依赖一致
□ 是否检查构建产物的 gzip 体积是否在合理范围
构建体积参考标准
| 项目规模 | 首屏 JS(gzip) | 首屏 CSS(gzip) | 总构建时间 |
|---|---|---|---|
| 小型项目 | <100KB | <30KB | <5s |
| 中型项目 | 100-300KB | 30-80KB | 5-15s |
| 大型项目 | 300-600KB | 80-150KB | 15-40s |
| 超大项目 | >600KB(需进一步优化) | >150KB(需进一步优化) | >40s(需优化) |
构建优化决策树
首屏加载慢?
├── JS 体积大
│ ├── 路由懒加载(最有效)
│ ├── 组件懒加载(次优)
│ ├── 检查大依赖(替换或按需)
│ └── CDN externals(稳定依赖)
│
├── CSS 体积大
│ ├── 使用未使用的 CSS(PurgeCSS)
│ ├── 拆分非首屏 CSS
│ └── 使用 CSS minifier
│
├── 图片加载慢
│ ├── 压缩图片(WebP/AVIF)
│ ├── 图片懒加载(IntersectionObserver)
│ └── 使用 CDN 图床
│
└── 请求数过多
├── 合并小 chunk
├── 使用 HTTP/2
└── 合理配置缓存策略
完整配置示例(Vite)
// vite.config.ts - 生产可用配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import ElementPlus from 'unplugin-element-plus/vite'
import { visualizer } from 'rollup-plugin-visualizer'
import viteCompression from 'vite-plugin-compression'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { cdn } from 'vite-plugin-cdn2'
export default defineConfig({
// 开发环境优化
optimizeDeps: {
include: [
'element-plus',
'echarts',
'dayjs',
],
},
plugins: [
vue(),
vueJsx(),
// Element Plus 按需导入
ElementPlus({
useSource: true,
}),
// 图片压缩
ViteImageOptimizer({
png: { quality: 80 },
jpeg: { quality: 80 },
webp: { quality: 75 },
}),
// Gzip 压缩
viteCompression({
algorithm: 'brotliCompress',
ext: '.br',
threshold: 1024,
compressionOptions: { level: 9 },
}),
// 包体积分析(仅分析时开启)
process.env.ANALYZE === 'true' && visualizer({
open: true,
gzipSize: true,
brotliSize: true,
}),
].filter(Boolean),
build: {
// 目标浏览器
target: 'es2015',
// 关闭 sourcemap
sourcemap: false,
// CSS 压缩
cssMinify: 'esbuild',
// 资源内联阈值
assetsInlineLimit: 4096,
// Rollup 配置
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-element': ['element-plus'],
'chart-vendor': ['echarts', 'vue-echarts'],
'utils-vendor': ['lodash-es', 'dayjs', 'axios'],
},
},
// esbuild 压缩配置
},
// 移除 console 和 debugger
minify: 'esbuild',
esbuild: {
drop: ['console', 'debugger'],
},
// Chunk 大小警告阈值
chunkSizeWarningLimit: 500,
},
// 路径别名
resolve: {
alias: {
'@': '/src',
},
},
})