跳到主要内容

性能优化

虽然 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
}
},
}
}

避免阻塞钩子

buildStartconfigconfigResolved 钩子不应该执行耗时操作。这些钩子在开发服务器启动期间被等待,会延迟你访问网站的时间。

优化转换钩子

resolveIdloadtransform 钩子可能导致某些文件加载比其他文件慢。虽然有时不可避免,但仍值得检查可能的优化区域。

优化策略

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

在浏览器中访问网站,加载完成后返回终端:

  1. p 键停止记录
  2. q 键退出

这会在项目根目录生成 vite-profile-0.cpuprofile 文件。你可以:

  1. 上传到 speedscope.app 进行可视化分析
  2. 与 Vite 团队分享以帮助识别性能问题

减少路径解析操作

解析导入路径在命中最坏情况时可能是昂贵的操作。

扩展名解析

Vite 支持通过 resolve.extensions 选项"猜测"导入路径,默认值为:

['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json']

当你尝试用 import './Component' 导入 ./Component.jsx 时,Vite 会执行以下步骤:

  1. 检查 ./Component 是否存在 → 否
  2. 检查 ./Component.mjs 是否存在 → 否
  3. 检查 ./Component.js 是否存在 → 否
  4. 检查 ./Component.mts 是否存在 → 否
  5. 检查 ./Component.ts 是否存在 → 否
  6. 检查 ./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-importno-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 在被请求时就已准备好并缓存,可以立即提供服务。

注意事项

  • 只预热频繁使用的文件,避免开发服务器启动时过载
  • 使用 --openserver.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 原生特性的优势

  1. 无需编译:原生 CSS 直接被浏览器解析,开发时无需预处理
  2. 更小的依赖:不需要安装 Sass、Less 等预处理器
  3. 标准化:编写的代码符合 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。