"为什么用了Vite,首屏还是慢?"去年接手一个中台项目时,我盯着Chrome DevTools里长达3秒的LCP指标,发现Vite的"闪电般"开发体验并没有自动转化为生产性能。经过两周的深度调优,最终将LCP从3s压到800ms。本文将分享那些不会出现在官方文档里的硬核优化手段。
1. 预构建:被忽视的性能黑洞
接手的是一个基于Vue 3 + Vite的物流管理后台,当依赖项增长到150+时,冷启动时间从1.5秒暴涨到8秒。问题出在非智能的预构建策略:
bash
# 错误做法:完全依赖自动发现
# vite.config.js
export default {
optimizeDeps: {
auto: true // 默认值,可能漏掉深层依赖
}
}
Vite的预构建机制是:首次启动时扫描入口文件的import语句,将CommonJS/UMD格式的依赖转换为ESM。但实际项目中:
- 动态导入(如
import('lib/' + variable))无法被静态分析 - 深层嵌套的依赖(如
A→B→C中B是ESM但C是CJS)可能被遗漏 - 未使用的依赖也被打包(如组件库的全量引入)
- 优化方案*:
javascript
// vite.config.js
export default {
optimizeDeps: {
include: [ // 显式声明需要预构建的依赖
'vue',
'lodash-es',
'element-plus/es/components/table',
'axios'
],
exclude: ['debug'], // 排除开发专用包
force: true // 首次启动强制重建
}
}
对比数据:
- 优化前:8秒(自动发现漏掉32个依赖)
- 优化后:2.1秒(全量预构建+排除干扰项)
- 避坑清单*:
- 不要依赖
auto: true处理Monorepo项目 node_modules/.vite缓存目录需要随依赖版本更新而清除- 动态路径引用的包(如
import('@libs/' + name))需手动声明 - 组件库按需引入时,必须包含所有用到的子组件路径
2. 分片策略:拆包的艺术
当产物超过20个chunk时,我们遇到了首次加载时大量小文件请求的问题。关键发现:Vite默认的manualChunks可能让你的HTTP/2优势变劣势。
典型反模式:
javascript
// 错误的分片方式
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router'],
utils: ['lodash', 'axios']
}
}
}
在HTTP/2环境下,这种"按类型分片"会导致:
- 并行请求过多(Chrome同一域名下最多6个TCP连接)
- 缓存利用率低(utils的变更使vendor缓存失效)
- 优化后的分片策略*:
javascript
function createCustomChunk(id) {
if (id.includes('node_modules')) {
const pkg = id.split('node_modules/')[1].split('/')[0]
if (pkg.includes('element-plus')) return 'ui'
return 'vendor'
}
if (id.includes('src/components')) return 'components'
}
export default {
build: {
rollupOptions: {
output: {
manualChunks: createCustomChunk
}
}
}
}
关键原则:
- 同包同chunk(如所有
element-plus组件打包到ui) - 业务组件单独分片(按路由或功能)
- 基础库保持稳定(vendor单独分片)
实测效果:
| 策略 | 请求数 | 缓存命中率 |
|---|---|---|
| 默认 | 28 | 62% |
| 优化后 | 14 | 89% |
3. 按需加载:动态导入的陷阱
在异步加载富文本编辑器的场景中,我们遇到了令人困惑的现象:明明用import()做了代码分割,但构建后的HTML里依然出现了预加载标签:
html
<!-- 意外的预加载 -->
<link rel="modulepreload" href="/src/components/Editor.vue" />
- 根因 *:Vite的默认预加载策略会分析所有可能的动态导入路径,对确定性路径(如
import('./Editor.vue'))自动生成modulepreload。这在SPA中反而会拖慢首屏。 - 解决方案*:
javascript
// 正确做法:对非首屏资源关闭预加载
export default {
build: {
rollupOptions: {
output: {
experimentalMinChunkSize: 1024 * 10, // 10KB以下不拆分
manualChunks: (id) => {
if (id.includes('RichTextEditor')) {
return 'editor'
}
}
}
}
}
}
同时配合路由级懒加载:
javascript
// router.js
{
path: '/edit',
component: () => import(/* webpackChunkName: "editor" */ './Editor.vue')
}
- 关键指标对比*:
- 优化前:首屏加载1.8MB(包含编辑器代码)
- 优化后:首屏1.2MB,编辑器延迟加载
4. 终极武器:SSR-friendly优化
当项目需要SSR时,直接使用vite build会导致性能退化。以下是我们在同构渲染中发现的三个致命问题:
- Client Hydration失败:SSR生成的DOM结构与客户端不匹配
- SSR构建产物冗余:包含客户端专用代码
- 内存泄漏 :
createApp()未正确销毁
- 推荐架构*:
javascript
// 客户端入口
import { createSSRApp } from 'vue'
import App from './App.vue'
if (!import.meta.env.SSR) {
createSSRApp(App).mount('#app')
}
// 服务端入口
export async function render(url) {
const { app, router } = createApp()
await router.push(url)
const html = await renderToString(app)
return html
}
- 构建配置关键点*:
javascript
// vite.config.js
export default {
build: {
ssr: true, // 标记SSR构建
ssrManifest: true, // 生成manifest文件
rollupOptions: {
input: 'src/entry-server.js' // 指定SSR入口
}
}
}
性能对比:
- CSR首屏:2.4s (TTFB 300ms)
- SSR优化后:1.1s (TTFB 80ms)
最后送上一句经验之谈:Vite不是魔法,它的快是有条件的------正确的依赖划分、精准的分片策略、SSR的深度优化,缺一不可。你在项目中是怎么处理这些痛点的?欢迎分享你的实战案例。