一、为什么 Vite 的性能思路不一样?
传统工具(Webpack)的做法是:开发时也打包。不管项目大小,启动时先把所有模块打包成一个 bundle,启动慢、改一行代码也要重新打包。
Vite 的核心理念是:开发和构建是两个不同的问题,用不同的工具解决。
Rolldown 还有一些兼容性问题,故暂时不讲 Rolldown 。
| 开发阶段 | 生产构建 | |
|---|---|---|
| 工具 | 浏览器原生 ESM + esbuild | Rollup(可插拔,生态成熟) |
| 做了什么 | 不打包,浏览器直接 import 单个文件,按需请求编译 |
全量打包 + Tree Shaking + 压缩 |
| 速度 | 启动 < 1s,HMR < 50ms | 比 Webpack 快 2-5x |
| 改一行代码 | 只编译那一个文件 | 无影响 |
二、源码层优化(开发体验)
2.1 依赖预构建
Vite 启动时会把 node_modules 里的依赖提前转成 ESM 并合并,减少浏览器请求数。
TypeScript
// vite.config.ts
export default defineConfig({
optimizeDeps: {
include: [ // 默认情况下,不在 node_modules 中链接的包不会预构建
'lodash-es', // 我们也可以强制预构建
'dayjs',
'echarts',
'@ant-design/icons-vue'
],
exclude: [
'vue', // 已知 ESM 的库不需要再处理
'pinia',
'vue-router'
]
}
})
2.2 持久化缓存
Vite 会把预构建产物和编译缓存写入 node_modules/.vite/,第二次启动直接读缓存。
2.3 多线程编译(esbuild / SWC)
Vite 开发模式用 esbuild 做 transform(TS → JS、JSX 转译等),它是 Go 写的,比 Babel 快 20 倍(官方说的,不是我说的)。
TypeScript
// 默认已启用,但可以调优
export default defineConfig({
esbuild: {
target: 'es2020',
minifyWhitespace: true, // 开发阶段也可压缩空白
treeShaking: true,
pure: ViteEnv.VITE_DROP_CONSOLE ? ["console.log", "debugger"] : [] // 打包时是否删除 console debugger
}
})
可以用 SWC 替代 Babel 优化性能:
TypeScript
// 安装 @vitejs/plugin-react-swc
import react from '@vitejs/plugin-react-swc'
export default defineConfig({
plugins: [react()] // 默认用 SWC 而不是 Babel
})
三、构建层优化(打包速度与质量)
3.1 chunk 分割 ------ 把「大礼包」拆成「小零食」
如果不做分割,所有代码打成一个大包(比如 5MB),用户打开页面要下载 5MB 才能渲染。
TypeScript
export default defineConfig({
build: {
outDir: "dist",
sourcemap: false, // 不生成 sourcemap 减少包体积
reportCompressedSize: false, // 不计算 gzip 后大小,加快打包速度
chunkSizeWarningLimit: 2000, // 默认 500KB,提升到 2000KB 减少警告
rollupOptions: {
output: {
manualChunks: {
// Vue 全家桶一个 chunk
'vue-vendor': ['vue', 'vue-router', 'pinia'],
// UI 组件库单独拆
'element-plus': ['element-plus'],
// 图表库单独拆(很大且不是所有页面都用)
'echarts': ['echarts', 'vue-echarts'],
// 工具库
'utils': ['lodash-es', 'dayjs', 'axios']
}
}
}
}
})
效果对比:
Plain
不分割: bundle.js = 3.2MB
分割后: vue-vendor.js (180KB) ← 所有页面共享,缓存
element-plus.js (1.1MB) ← 用了表单页才下载
echarts.js (980KB) ← 只有报表页才下载
index.js (40KB) ← 首页只需这个
3.2 生产方式改用 esbuild 压缩
TypeScript
export default defineConfig({
build: {
minify: 'esbuild', // 现在默认是 'esbuild',比以前默认 terser 快很多
target: 'es2015', // 目标浏览器
cssMinify: 'esbuild', // CSS 也用 esbuild 压缩
}
})
3.3 移除调试代码
TypeScript
export default defineConfig({
build: {
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // 移除 console.log
drop_debugger: true
}
}
}
})
注:线上保留
console.warn/console.error是有争议的,通常是保留错误日志(方便监控),去掉调试日志。esbuild / terser 都是全量删除所有 console.*。 想精细控制(只删 log 保留 error)需要自己写 Vite 插件。
四、产物层优化(加载性能)
4.1 路由级懒加载 ------ 首屏只加载需要的代码
TypeScript
// router/index.ts
const routes = [
{
path: '/',
component: () => import('@/views/Home.vue') // 首页
},
{
path: '/orders',
component: () => import('@/views/OrderList.vue') // 订单管理页
},
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue') // 数据看板页
}
]
Vite 遇到 () => import(...) 会自动生成单独的 chunk。用户访问首页只下载首页的 chunk,切换到订单页才下载订单页的 chunk。
4.2 组件级懒加载 ------ 非关键组件按需加载
Plain
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
// 这个组件很大(富文本编辑器、图表等),只在需要时加载
const RichEditor = defineAsyncComponent(() =>
import('@/components/RichEditor.vue')
)
</script>
<template>
<RichEditor v-if="showEditor" />
</template>
4.3 Brotli / Gzip 压缩
TypeScript
// 安装 vite-plugin-compression
import compression from 'vite-plugin-compression'
export default defineConfig({
plugins: [
compression({
algorithm: 'brotliCompress', // Brotli 比 Gzip 多压缩 15-20%
threshold: 1024, // 只压缩 >1KB 的文件
deleteOriginFile: false, // 保留原始文件作为 fallback
})
]
})
Nginx 配置自动选择最优压缩:
Nginx
# 客户端支持 Brotli 就用 .br,不支持就用 .gz
location /assets {
gzip_static on;
brotli_static on;
expires 1y;
add_header Cache-Control "public, immutable";
}
五、部署层优化(网络性能)
5.1 CDN + 缓存策略
TypeScript
// vite.config.ts 构建时 external 大库
export default defineConfig({
build: {
rollupOptions: {
external: ['vue', 'element-plus'], // 不打入 bundle
output: {
globals: {
vue: 'Vue',
'element-plus': 'ElementPlus'
}
}
}
}
})
HTML
<!-- index.html 用 CDN 引入 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.4/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/element-plus"></script>
5.2 图片优化
TypeScript
// vite-plugin-imagemin 自动压缩图片
import imagemin from 'vite-plugin-imagemin'
export default defineConfig({
plugins: [
imagemin({
gifsicle: { optimizationLevel: 7 },
optipng: { optimizationLevel: 7 },
mozjpeg: { quality: 80 },
pngquant: { quality: [0.8, 0.9] },
webp: { quality: 80 } // 生成 WebP 格式
})
]
})
配合 HTML 原生懒加载:
Plain
<img loading="lazy" src="/images/product.jpg" alt="商品图片" />
5.3 关键资源预加载
TypeScript
// vite.config.ts 自动插入 preload 标签
export default defineConfig({
plugins: [
// Vite 默认会为入口 chunk 和 CSS 生成 preload
// 还可以手动预加载关键资源
]
})
HTML
<!-- 构建产物自动生成类似这样的 preload -->
<link rel="modulepreload" href="/assets/index.abc123.js">
<link rel="preload" href="/assets/font.woff2" as="font" crossorigin>
<link rel="prefetch" href="/assets/orders.chunk.js"> <!-- 下一页预加载 -->
5.4 RUM 监控 ------ 用真实数据驱动优化
一般来说,我们上线前不会"凭感觉"优化,而是先埋点,用真实用户数据指导方向:
TypeScript
// 采集 Web Vitals 指标
import { onLCP, onFID, onCLS, onFCP, onTTFB } from 'web-vitals'
onLCP(metric => reportMetric('LCP', metric.value)) // 最大内容绘制
onFID(metric => reportMetric('FID', metric.value)) // 首次输入延迟
onCLS(metric => reportMetric('CLS', metric.value)) // 布局偏移
onFCP(metric => reportMetric('FCP', metric.value)) // 首次内容绘制
核心原则:优化什么,由数据决定,不是由感觉决定。
六、其他配置
6.1 路径别名
TypeScript
resolve: {
alias: {
"@": resolve(__dirname, "./src")
}
}
@→./src:这样可以在代码里写import X from '@/components/xxx'而不是../../../components/xxx
6.2 SCSS 全局变量注入
TypeScript
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/var.scss";`
}
}
}
作用: 每个 SCSS 文件编译时,自动在最前面插入 @import "@/styles/var.scss",这样所有组件都能直接使用其中的变量($primary-color 等),不需要每个文件手动 import。
这里的
@import是 SCSS 的,不是 CSS 的,注意区分。
6.3 开发服务器配置
TypeScript
server: {
allowedHosts: true, // 允许任何域名访问(谨慎配置)
hmr: true, // 热更新
host: '0.0.0.0', // 监听所有网卡
port: 8080,
open: true, // 项目启动直接开启浏览器页面
cors: true, // 允许跨域
// proxy: xxx // 代理(开发环境一般反向代理)
}
6.4 插件
TypeScript
plugins: xxx, // 插件