性能优化
虽然 Vite 默认就很快,但随着项目规模增长,性能问题可能会逐渐显现。本章将帮助你识别和解决常见的性能问题,包括:
- 开发服务器启动慢
- 页面加载慢
- 构建速度慢
检查浏览器设置
在深入优化之前,先确保浏览器配置没有成为性能瓶颈。
浏览器扩展
某些浏览器扩展可能会干扰请求,导致大型应用的启动和重载变慢,特别是在打开开发者工具时。
建议:
- 创建一个无扩展的开发专用浏览器配置文件
- 使用无痕模式进行开发
无痕模式比没有扩展的普通配置文件更快,因为它不包含任何浏览历史、缓存或其他可能影响性能的数据。
浏览器缓存
Vite 开发服务器对预构建的依赖进行强缓存,对源代码实现快速的 304 响应。
注意:如果在浏览器开发者工具中打开了"禁用缓存"选项,会对启动和完整页面重载时间产生很大影响。请确保在使用 Vite 开发服务器时没有启用"禁用缓存"。
审查 Vite 插件
Vite 的内部插件和官方插件都经过优化,尽可能减少工作量。例如,代码转换在开发时使用正则表达式,而在构建时进行完整解析以确保正确性。
然而,社区插件的性能不在 Vite 的控制范围内,可能会影响开发体验。以下是使用插件时需要注意的几点:
动态导入大型依赖
只在特定情况下使用的大型依赖应该动态导入,以减少 Node.js 启动时间。
// ❌ 不好:顶部导入大型依赖
import heavyLib from 'heavy-lib'
export function myPlugin() {
return {
name: 'my-plugin',
transform(code, id) {
if (id.endsWith('.special')) {
// 使用 heavyLib
}
},
}
}
// ✅ 好:动态导入
export function myPlugin() {
return {
name: 'my-plugin',
async transform(code, id) {
if (id.endsWith('.special')) {
const heavyLib = await import('heavy-lib')
// 使用 heavyLib
}
},
}
}
避免阻塞钩子
buildStart、config、configResolved 钩子不应该执行耗时操作。这些钩子在开发服务器启动期间被等待,会延迟你访问网站的时间。
优化转换钩子
resolveId、load、transform 钩子可能导致某些文件加载比其他文件慢。虽然有时不可避免,但仍值得检查可能的优化区域。
优化策略:
export function myPlugin() {
return {
name: 'my-plugin',
transform(code, id) {
// ❌ 不好:直接进行完整转换
// return transformCode(code)
// ✅ 好:先快速过滤
if (!id.endsWith('.special')) return null
// 再检查是否需要转换
if (!code.includes('SPECIAL_TAG')) return null
// 最后执行转换
return transformCode(code)
},
}
}
转换文件的时间越长,在浏览器中加载网站时的请求瀑布流就越显著。
检查插件性能
使用调试命令检查插件转换耗时:
vite --debug plugin-transform
或使用 vite-plugin-inspect 插件:
npm install -D vite-plugin-inspect
// vite.config.js
import Inspect from 'vite-plugin-inspect'
export default defineConfig({
plugins: [Inspect()],
})
访问 http://localhost:5173/__inspect/ 查看每个文件的转换时间和结果。
注意:由于异步操作往往提供不准确的计时,你应该将这些数字视为粗略估计,但它仍然可以揭示较昂贵的操作。
性能分析
运行性能分析,记录 CPU 配置文件:
vite --profile --open
在浏览器中访问网站,加载完成后返回终端:
- 按
p键停止记录 - 按
q键退出
这会在项目根目录生成 vite-profile-0.cpuprofile 文件。你可以:
- 上传到 speedscope.app 进行可视化分析
- 与 Vite 团队分享以帮助识别性能问题
减少路径解析操作
解析导入路径在命中最坏情况时可能是昂贵的操作。
扩展名解析
Vite 支持通过 resolve.extensions 选项"猜测"导入路径,默认值为:
['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json']
当你尝试用 import './Component' 导入 ./Component.jsx 时,Vite 会执行以下步骤:
- 检查
./Component是否存在 → 否 - 检查
./Component.mjs是否存在 → 否 - 检查
./Component.js是否存在 → 否 - 检查
./Component.mts是否存在 → 否 - 检查
./Component.ts是否存在 → 否 - 检查
./Component.jsx是否存在 → 是!
总共需要 6 次文件系统检查才能解析一个导入路径。隐式导入越多,累计的解析时间就越长。
解决方案
方案一:显式导入路径
// ❌ 隐式扩展名
import Component from './Component'
// ✅ 显式扩展名
import Component from './Component.jsx'
方案二:缩小扩展名列表
// vite.config.js
export default defineConfig({
resolve: {
extensions: ['.js', '.ts', '.jsx', '.tsx', '.json'],
},
})
注意:确保缩小后的列表仍然能正确处理 node_modules 中的文件。
TypeScript 项目:
启用 moduleResolution: "bundler" 和 allowImportingTsExtensions: true,可以直接在代码中使用 .ts 和 .tsx 扩展名:
// tsconfig.json
{
"compilerOptions": {
"moduleResolution": "bundler",
"allowImportingTsExtensions": true
}
}
// 现在可以直接使用 .ts 扩展名
import { util } from './utils/helper.ts'
插件开发者注意事项
如果你是插件作者,确保只在需要时调用 this.resolve,以减少上述检查次数。
避免 Barrel 文件
Barrel 文件(桶文件)是在同一目录中重新导出其他文件 API 的文件。
// utils/index.js - 这是一个 barrel 文件
export * from './color.js'
export * from './dom.js'
export * from './slash.js'
问题分析
当你只导入一个单独的 API 时,例如:
import { slash } from './utils'
Barrel 文件中的所有文件都需要被获取和转换,因为它们可能包含 slash API,也可能包含在初始化时运行的副作用。这意味着你在初始页面加载时加载了比需要更多的文件,导致页面加载变慢。
解决方案
直接导入具体的文件:
// ❌ 从 barrel 文件导入
import { slash } from './utils'
// ✅ 直接导入具体文件
import { slash } from './utils/slash.js'
这确保只加载和转换实际需要的文件。
自动修复
如果你使用 ESLint,可以配置 eslint-plugin-import 的 no-barrel-files 规则来禁止 barrel 文件:
// eslint.config.js
import imports from 'eslint-plugin-import'
export default [
{
plugins: { import: imports },
rules: {
'import/no-barrel-files': 'error',
},
},
]
预热常用文件
Vite 开发服务器只转换浏览器请求的文件,这使其能够快速启动并只对使用的文件应用转换。它也可以在预期某些文件很快会被请求时预转换文件。
然而,如果某些文件的转换时间比其他文件长,请求瀑布流仍然可能发生。
瀑布流示例
假设有一个导入图,左边文件导入右边文件:
main.js -> BigComponent.vue -> big-utils.js -> large-data.json
导入关系只能在文件转换后才知道。如果 BigComponent.vue 需要一些时间转换,big-utils.js 必须等待,依此类推。即使有内置的预转换,这也会导致内部瀑布流。
解决方案
使用 server.warmup 选项预热你知道会频繁使用的文件:
# 找出哪些文件加载耗时
vite --debug transform
输出示例:
vite:transform 28.72ms /@vite/client +1ms
vite:transform 62.95ms /src/components/BigComponent.vue +1ms
vite:transform 102.54ms /src/utils/big-utils.js +1ms
根据日志配置预热:
// vite.config.js
export default defineConfig({
server: {
warmup: {
clientFiles: [
'./src/components/BigComponent.vue',
'./src/utils/big-utils.js',
],
},
},
})
这样,big-utils.js 在被请求时就已准备好并缓存,可以立即提供服务。
注意事项
- 只预热频繁使用的文件,避免开发服务器启动时过载
- 使用
--open或server.open也会提供性能提升,因为 Vite 会自动预热应用的入口点
使用更少或原生工具
保持 Vite 快速的关键是减少对源文件(JS/TS/CSS)的处理量。每次文件转换都需要时间,减少转换步骤就能提升性能。
减少处理工作
使用 CSS 替代预处理器:
如果可能,使用纯 CSS 而不是 Sass/Less/Stylus。CSS 原生现在支持嵌套(通过 PostCSS / Lightning CSS 处理)。
/* 现代 CSS 原生支持嵌套 */
.container {
&:hover {
background: blue;
}
.title {
font-size: 24px;
}
}
CSS 原生特性的优势:
- 无需编译:原生 CSS 直接被浏览器解析,开发时无需预处理
- 更小的依赖:不需要安装 Sass、Less 等预处理器
- 标准化:编写的代码符合 CSS 规范,未来兼容性好
什么时候仍然需要预处理器:
- 复杂的变量计算和数学运算
- 成熟的 mixin 库(如 Compass)
- 项目已有大量预处理器代码
避免转换 SVG:
不要将 SVG 转换为 UI 框架组件(React、Vue 等)。直接导入为字符串或 URL:
// ❌ 转换为组件(慢,每次都需要解析和转换)
import Logo from './logo.svg?react'
// ✅ 导入为 URL(快,直接返回路径)
import logoUrl from './logo.svg?url'
// ✅ 导入为字符串(快,直接读取文件内容)
import logoString from './logo.svg?raw'
SVG 处理性能对比:
| 方式 | 开发时处理 | 生产构建 | 适用场景 |
|---|---|---|---|
?react 或 ?vue | 慢(需要编译) | 慢 | 需要动态修改 SVG 属性 |
?url | 快(返回路径) | 快 | 简单展示图标 |
?raw | 快(返回字符串) | 快 | 需要解析 SVG 内容 |
使用原生工具
Vite 核心基于原生工具构建,但某些功能为了更好的兼容性和功能集,默认仍使用非原生工具。对于大型应用,可能值得付出这些成本。
Lightning CSS:
尝试实验性的 Lightning CSS 支持:
npm install -D lightningcss
// vite.config.js
export default defineConfig({
css: {
transformer: 'lightningcss',
},
build: {
cssMinify: 'lightningcss',
},
})
Lightning CSS 是用 Rust 编写的极快 CSS 处理器,可以显著提升 CSS 处理速度。
Lightning CSS 的性能优势:
- CSS 解析速度比 PostCSS 快 10-100 倍
- 自动添加浏览器前缀
- 支持 CSS 嵌套语法
- 更小的安装体积
注意事项:
Lightning CSS 目前是实验性功能,可能与某些 PostCSS 插件不兼容。如果你的项目依赖特定的 PostCSS 插件,需要先测试兼容性。
禁用不必要的功能:
export default defineConfig({
build: {
// 如果不需要 source map,禁用可以显著提升构建速度
sourcemap: false,
// 禁用压缩大小报告(减少 I/O 操作)
reportCompressedSize: false,
// 如果不需要 CSS 代码分割
cssCodeSplit: false,
},
css: {
// 开发时不生成 CSS source map
devSourcemap: false,
},
})
Full Bundle Mode(实验性)
Vite 8 引入了实验性的 Full Bundle Mode,这是对 Vite 传统开发模式的一次重要革新。
背景:为什么需要 Full Bundle Mode?
Vite 以其非打包开发服务器(unbundled dev server)闻名,这是 Vite 首次推出时速度和受欢迎的主要原因。这种方法的核心理念是:开发时不打包源代码,让浏览器直接加载 ES 模块。
然而,随着项目规模和复杂度的增长,两个主要挑战浮现出来:
开发/生产不一致:开发时提供的非打包 JavaScript 与生产环境的打包构建产生不同的运行时行为。这可能导致只在生产环境中出现的问题,使调试变得困难。
开发时性能下降:非打包方法导致每个模块被单独获取,产生大量网络请求。虽然这对生产环境没有影响,但在开发服务器启动和页面刷新时会造成显著的开销。在大型应用中,可能需要处理数百甚至数千个独立请求。当开发者使用网络代理时,这些瓶颈会变得更加严重。
Full Bundle Mode 的优势
Full Bundle Mode 允许在开发环境也提供打包后的文件,结合了两者的优点:
- 更快的启动时间:即使是大型应用也能快速启动
- 开发和生产行为一致:减少只在生产环境出现的问题
- 减少网络开销:页面刷新时更少的网络请求
- 保持高效的 HMR:在 ESM 输出之上仍然高效
初步性能数据
在早期测试中,Full Bundle Mode 显示出显著的性能改进:
- 开发服务器启动快 3 倍
- 完整页面重载快 40%
- 网络请求减少 10 倍
这些改进对于大型项目尤为明显,传统的非打包开发方法在这些项目中会遇到扩展限制。
如何启用
// vite.config.js
export default defineConfig({
experimental: {
enableFullBundleMode: true,
},
})
注意事项
Full Bundle Mode 目前是实验性功能。类似于 Rolldown 集成,Vite 团队计划在收集反馈并确保稳定性后,将其作为默认选项。
如果你在使用 Full Bundle Mode 时遇到问题,可以通过禁用该选项回退到传统的非打包模式。
构建性能优化
减少构建工作量
// vite.config.js
export default defineConfig({
build: {
// 禁用 source map(显著加快构建)
sourcemap: false,
// 禁用压缩大小报告
reportCompressedSize: false,
// 使用 esbuild 压缩(比 terser 快)
minify: 'esbuild',
// 提高 chunk 大小警告阈值
chunkSizeWarningLimit: 1000,
},
})
优化代码分割
合理的代码分割可以提升加载性能:
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
// 将大型依赖单独打包
if (id.includes('node_modules/large-lib')) {
return 'large-lib'
}
// 将 node_modules 分组
if (id.includes('node_modules')) {
return 'vendor'
}
},
},
},
},
})
并行处理
确保充分利用多核 CPU:
export default defineConfig({
build: {
// 启用 CSS 代码分割(默认开启)
cssCodeSplit: true,
// 并行处理 chunk
rollupOptions: {
output: {
// 控制分割粒度
manualChunks: 'auto',
},
},
},
})
性能监控
持续监控
使用 CI/CD 流程中的性能监控:
# GitHub Actions 示例
- name: Build
run: npm run build
- name: Report bundle size
run: npx bundlesize
构建分析
使用可视化工具分析构建产物:
npm install -D rollup-plugin-visualizer
// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
}),
],
})
构建后会自动打开可视化报告,展示每个模块的大小占比。
写在最后
性能优化是一个持续的过程。随着项目规模增长,可能会遇到启动慢、加载慢、构建慢等问题。
定位性能瓶颈可以使用 vite --profile 进行性能分析,或使用 vite-plugin-inspect 查看各插件的转换耗时。常见的优化手段包括:避免 barrel 文件、预热常用文件、显式指定文件扩展名、使用 Lightning CSS 等。对于大型项目,可以尝试实验性的 Full Bundle Mode。