Vite 凭借基于 ESM 的开发服务器和 Rollup 驱动的生产构建,已经成为现代前端项目的首选构建工具。然而,随着项目规模增长,默认配置往往无法满足性能需求。本文将从代码分割、Tree Shaking、构建配置、资源加载等多个维度,详细介绍 Vite 生产构建的优化策略。
一、代码分割与动态导入
代码分割(Code Splitting)是首屏加载优化的核心手段。通过动态 import() 语法,Vite 会自动将被导入模块拆分为独立 chunk,实现按需加载。
// 路由级别的懒加载
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
component: () => import('../views/Home.vue')
},
{
path: '/dashboard',
component: () => import('../views/Dashboard.vue')
},
{
path: '/settings',
component: () => import('../views/Settings.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
每个路由对应的组件会在用户首次访问该路由时才加载,大幅减少首屏 JavaScript 体积。配合 Webpack 风格的魔法注释,还可以自定义 chunk 名称:
// 自定义 chunk 名称
const Chart = () => import(
/* webpackChunkName: "chart" */
'../components/HeavyChart.vue'
)
二、手动分包策略
Vite 默认会将 node_modules 中的依赖打包到一个 vendor chunk 中。但对于大型项目,这个 vendor chunk 可能非常臃肿。通过 manualChunks 配置,可以更精细地控制分包策略。
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
output: {
manualChunks(id) {
// 将 React/Vue 等框架核心单独拆包(长期缓存)
if (id.includes('node_modules/vue/') ||
id.includes('node_modules/@vue/')) {
return 'vue-vendor'
}
// 将 UI 组件库单独拆包
if (id.includes('node_modules/element-plus/')) {
return 'element-plus'
}
// 将工具库单独拆包
if (id.includes('node_modules/lodash-es/') ||
id.includes('node_modules/dayjs/')) {
return 'utils'
}
// 将图表库单独拆包
if (id.includes('node_modules/echarts/') ||
id.includes('node_modules/zrender/')) {
return 'echarts'
}
// 其余第三方依赖
if (id.includes('node_modules/')) {
return 'vendor'
}
}
}
}
}
})
分包的核心原则是:变化频率不同的模块分开打包。框架代码很少变化,可以设置较长的缓存时间;业务代码频繁更新,单独打包后不会导致框架缓存失效。
三、Tree Shaking 与副作用标记
Vite 在生产模式下基于 Rollup 自动执行 Tree Shaking,移除未被使用的导出代码。但要确保其生效,需要注意以下几点。
// 确保使用 ES Module 导入(而非默认导入整个库)
// 不推荐:导入整个 lodash
import _ from 'lodash' // Tree Shaking 无效
// 推荐:具名导入具体函数
import { debounce, throttle } from 'lodash-es'
// 或者使用子路径导入
import debounce from 'lodash/debounce'
如果包的 package.json 中没有正确设置 "sideEffects": false,Tree Shaking 可能无法生效。对于自己的库项目,务必在 package.json 中声明副作用:
{
"name": "my-ui-lib",
"sideEffects": false,
"sideEffects": ["*.css", "./src/polyfills.js"]
}
四、Chunk 大小控制与警告处理
当单个 chunk 超过 500KB 时,Vite 构建会输出警告。可以通过 chunkSizeWarningLimit 调整阈值,但更好的方式是真正优化 chunk 大小。
export default defineConfig({
build: {
// 调整 chunk 大小警告阈值(单位 KB)
chunkSizeWarningLimit: 600,
rollupOptions: {
output: {
// 文件命名策略(利用缓存)
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
assetFileNames: '[ext]/[name]-[hash].[ext]'
}
},
// CSS 代码分割(默认开启)
cssCodeSplit: true,
// 资源内联阈值(小于 4KB 的资源转为 base64)
assetsInlineLimit: 4096,
// 禁用 CSS 内联到 JS(避免 FOUC)
cssMinify: 'lightningcss'
}
})
五、Preload 与 Prefetch 策略
Vite 默认会为动态导入的 chunk 添加 prefetch 链接,在浏览器空闲时预加载。但对于大型应用,过多的 prefetch 可能浪费带宽。可以按需调整这一行为。
// vite.config.js
export default defineConfig({
build: {
// 禁用所有 prefetch
modulePreload: {
polyfill: false
}
}
})
// 在组件中手动控制预加载
// 只对关键路由预加载
const router = createRouter({ /* ... */ })
router.beforeResolve((to) => {
// 鼠标悬停在链接上时预加载
})
// 或者使用 link 标签手动控制
// <link rel="preload" href="/js/dashboard-[hash].js" as="script">
// <link rel="prefetch" href="/js/settings-[hash].js">
preload 表示当前页面马上需要的资源,优先级高;prefetch 表示未来可能需要的资源,优先级低,浏览器空闲时才加载。合理使用这两种策略可以显著改善页面加载体验。
六、构建分析与持续优化
优化不能凭感觉,需要数据驱动。推荐使用 rollup-plugin-visualizer 生成构建产物分析报告。
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
vue(),
visualizer({
filename: './dist/stats.html',
open: true,
gzipSize: true,
brotliSize: true
})
]
})
运行 vite build 后会自动打开可视化报告页面,可以直观地看到每个 chunk 中包含的模块及其大小占比。根据报告结果调整分包策略、排查意外引入的大型依赖。
总结
Vite 构建优化是一个系统工程:代码分割减少首屏加载量,精细分包优化缓存命中率,Tree Shaking 移除无用代码,preload/prefetch 策略优化资源加载时机。建议在项目初期就建立构建产物分析的习惯,每次发版前检查 bundle 大小变化,避免性能退化。配合 CI 中的 bundle 大小限制检查,可以确保应用的加载性能始终保持在合理范围内。