前言

Vite 自诞生以来,凭借其”开发环境原生 ESM + 生产环境 Rollup 打包”的双轨架构,为前端开发带来了革命性的体验提升。然而,这种双轨架构也带来了一些问题:

  • 开发/生产环境不一致:esbuild(dev)与 Rollup(build)的行为差异可能导致线上问题
  • 性能瓶颈:大型项目的生产构建仍然较慢
  • 工具链碎片化:需要维护多个工具的配置和插件

为了解决这些问题,Vite 团队推出了新一代技术栈:

  • Rolldown:基于 Rust 的统一打包器,替代 esbuild + Rollup
  • OXC:高性能 JavaScript/TypeScript 工具集合
  • Vitest:与 Vite 深度集成的测试框架

背景:Void(0) 统一工具链愿景

这些技术的诞生并非孤立事件,而是 Vue 团队更宏大愿景的一部分。尤雨溪在 2023 年提出了 Void(0) 项目,一个类似 Rust Cargo 的统一前端工具链系统。

Void(0) 的核心理念:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
graph TB
A[Void 0 统一工具链] --> B[Rolldown 打包器]
A --> C[OXC 编译器基础设施]
A --> D[Vitest 测试框架]
A --> E[oxlint 代码检查]
A --> F[oxc-formatter 格式化]

B --> G[统一的开发体验]
C --> G
D --> G
E --> G
F --> G

style A fill:#f96,stroke:#333,stroke-width:3px
style G fill:#9f9,stroke:#333,stroke-width:2px

为什么需要 Void(0)?

  1. 生态碎片化:前端工具链过于分散(Webpack、Rollup、esbuild、SWC、Babel 等)
  2. 配置地狱:每个工具都有独立配置,维护成本高
  3. 性能瓶颈:JavaScript 编写的工具在大型项目中性能不足
  4. 一致性问题:开发和生产环境使用不同工具导致行为差异

Void(0) 的目标:

  • 🎯 统一配置:一份配置文件适用于所有工具
  • 🎯 原生性能:全部使用 Rust 实现,性能提升 10-100 倍
  • 🎯 完整工具链:覆盖开发、构建、测试、检查、格式化全流程
  • 🎯 渐进式迁移:兼容现有生态,平滑过渡

:Rolldown 由 Vue 团队主导开发,OXC 由字节跳动团队开发并与 Void(0) 深度协作,两者共同构成了新一代前端工具链的基石。

为什么需要 Rolldown

旧架构的痛点

在 Vite 6 之前,Vite 内部使用了两个打包工具:

1
2
3
4
5
6
7
8
9
graph LR
A[源代码] --> B{环境}
B -->|开发环境| C[esbuild]
B -->|生产环境| D[Rollup]
C --> E[开发服务器]
D --> F[生产构建产物]

style C fill:#f9f,stroke:#333
style D fill:#9ff,stroke:#333

主要问题:

  1. 行为不一致:esbuild 和 Rollup 对模块解析、Tree-shaking、代码分割的处理逻辑不同
  2. 插件生态割裂:需要同时维护 esbuild 插件和 Rollup 插件
  3. 性能开销:两套工具链意味着双倍的解析、转换开销
  4. 配置复杂:需要分别配置 optimizeDeps(esbuild)和 build(Rollup)

Rolldown 的统一愿景

Rolldown 的目标是提供单一、高性能、兼容 Rollup 的打包器,统一开发和生产环境:

1
2
3
4
5
6
7
8
9
graph LR
A[源代码] --> B[Rolldown]
B --> C{模式}
C -->|开发模式| D[快速增量构建]
C -->|生产模式| E[优化打包]
D --> F[开发服务器]
E --> G[生产构建产物]

style B fill:#0f0,stroke:#333

核心优势:

  • 统一行为:开发和生产使用同一套代码路径
  • 原生性能:Rust 实现,比 Rollup 快 3-16 倍
  • 插件兼容:支持大部分 Rollup 插件
  • 内存优化:内存使用减少高达 100 倍

Rolldown:基于 Rust 的统一打包器

核心架构

Rolldown 采用 Rust 编写,基于 OXC 工具链构建,架构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
graph TB
A[入口文件] --> B[OXC Parser]
B --> C[AST]
C --> D[OXC Transformer]
D --> E[转换后的 AST]
E --> F[模块图构建]
F --> G[Tree-shaking]
G --> H[代码分割]
H --> I[OXC Minifier]
I --> J[输出产物]

style B fill:#ff9
style D fill:#ff9
style I fill:#ff9

底层原理详解

模块图构建(Module Graph)

Rolldown 的核心是构建一个高效的模块依赖图,这是所有后续优化的基础。

模块图数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Rolldown 内部模块图结构(简化)
pub struct ModuleGraph {
// 模块 ID -> 模块信息的映射
modules: HashMap<ModuleId, Module>,
// 依赖关系图
dependencies: HashMap<ModuleId, Vec<ModuleId>>,
// 入口模块
entries: Vec<ModuleId>,
}

pub struct Module {
id: ModuleId,
path: PathBuf,
// 抽象语法树
ast: Program,
// 导出的符号
exports: Vec<Export>,
// 导入的符号
imports: Vec<Import>,
// 副作用标记
has_side_effects: bool,
}

构建流程:

1
2
3
4
5
6
7
8
9
10
11
12
graph TB
A[入口模块] --> B[解析模块]
B --> C[提取 import/export]
C --> D{是否有新依赖?}
D -->|是| E[解析依赖模块]
E --> B
D -->|否| F[构建完成]

B --> G[缓存模块 AST]
C --> H[记录依赖关系]

style F fill:#9f9

关键优化:

  1. 并行解析:利用 Rust 的并发能力,多线程解析模块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 并行解析示例
pub async fn parse_modules_parallel(
paths: Vec<PathBuf>
) -> Result<Vec<Module>> {
let tasks: Vec<_> = paths
.into_iter()
.map(|path| {
tokio::spawn(async move {
parse_module(path).await
})
})
.collect();

// 等待所有解析任务完成
let results = futures::future::join_all(tasks).await;
// ...
}
  1. 增量更新:HMR 时只重新解析变化的模块
1
2
3
4
5
6
7
8
9
10
11
12
pub fn update_module(&mut self, id: ModuleId, new_ast: Program) {
// 只更新变化的模块
if let Some(module) = self.modules.get_mut(&id) {
module.ast = new_ast;
// 重新分析导出/导入
module.exports = extract_exports(&new_ast);
module.imports = extract_imports(&new_ast);
}

// 标记受影响的模块需要重新打包
self.mark_affected_modules(id);
}

Tree-shaking

Rolldown 实现了比传统工具更激进的 Tree-shaking,基于 标记-清除(Mark-Sweep) 算法。

算法流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
graph TB
A[入口模块] --> B[标记阶段 Mark]
B --> C[从入口开始遍历]
C --> D[标记使用的导出]
D --> E[递归标记依赖]
E --> F{还有未访问的依赖?}
F -->|是| E
F -->|否| G[清除阶段 Sweep]
G --> H[移除未标记的代码]
H --> I[生成最终产物]

style B fill:#ff9
style G fill:#f96

核心实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// Tree-shaking 实现(简化)
pub struct TreeShaker {
module_graph: ModuleGraph,
// 标记哪些符号被使用
used_symbols: HashSet<SymbolId>,
}

impl TreeShaker {
// 标记阶段:从入口开始标记所有使用的符号
pub fn mark_phase(&mut self, entry: ModuleId) {
let mut queue = VecDeque::new();
queue.push_back(entry);

while let Some(module_id) = queue.pop_front() {
let module = &self.module_graph.modules[&module_id];

// 遍历模块中的所有语句
for stmt in &module.ast.body {
match stmt {
// 标记使用的导入
Stmt::Import(import) => {
for specifier in &import.specifiers {
self.mark_symbol(specifier.local);
// 追踪到源模块
if let Some(source_module) =
self.resolve_import(&import.source) {
queue.push_back(source_module);
}
}
}
// 标记使用的变量引用
Stmt::Expr(expr) => {
self.mark_used_identifiers(expr);
}
_ => {}
}
}
}
}

// 清除阶段:移除未使用的代码
pub fn sweep_phase(&mut self) -> Program {
let mut new_program = Program::new();

for module in self.module_graph.modules.values() {
for stmt in &module.ast.body {
// 只保留被标记的语句
if self.is_statement_used(stmt) {
new_program.body.push(stmt.clone());
}
}
}

new_program
}

// 判断语句是否被使用
fn is_statement_used(&self, stmt: &Stmt) -> bool {
match stmt {
// 导出声明:检查导出的符号是否被使用
Stmt::ExportDecl(export) => {
self.used_symbols.contains(&export.symbol_id)
}
// 函数声明:检查函数是否被调用
Stmt::FnDecl(func) => {
self.used_symbols.contains(&func.ident.symbol_id)
}
// 副作用语句:始终保留
Stmt::Expr(_) => true,
_ => false,
}
}
}

高级优化:

  1. 副作用分析:识别纯函数和有副作用的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 副作用分析
pub fn analyze_side_effects(expr: &Expr) -> bool {
match expr {
// 字面量无副作用
Expr::Lit(_) => false,
// 函数调用可能有副作用
Expr::Call(call) => {
// 检查是否是已知的纯函数
if is_pure_function(&call.callee) {
false
} else {
true
}
}
// 赋值有副作用
Expr::Assign(_) => true,
_ => false,
}
}
  1. 跨模块优化:内联小型模块
1
2
3
4
5
6
7
8
9
10
// 模块内联
pub fn inline_small_modules(&mut self) {
for module in &self.module_graph.modules {
// 如果模块很小且只被一个地方使用
if module.size < 1000 && module.import_count == 1 {
// 直接内联到使用处
self.inline_module(module.id);
}
}
}

代码分割(Code Splitting)

Rolldown 的代码分割算法基于 模块依赖图分析启发式规则

分割策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
graph TB
A[分析模块依赖] --> B[识别共享模块]
B --> C[计算模块权重]
C --> D[应用分割规则]
D --> E{是否满足条件?}
E -->|是| F[创建新 Chunk]
E -->|否| G[合并到现有 Chunk]
F --> H[优化 Chunk 大小]
G --> H
H --> I[生成最终 Chunks]

style F fill:#9f9
style I fill:#9f9

核心算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
pub struct ChunkGraph {
chunks: Vec<Chunk>,
module_to_chunk: HashMap<ModuleId, ChunkId>,
}

pub struct Chunk {
id: ChunkId,
modules: Vec<ModuleId>,
size: usize,
// 依赖的其他 chunks
dependencies: Vec<ChunkId>,
}

impl ChunkGraph {
// 基于共享依赖创建 chunks
pub fn create_chunks(&mut self, config: &ChunkConfig) {
// 1. 为每个入口创建初始 chunk
for entry in &self.module_graph.entries {
let chunk = Chunk::new(*entry);
self.chunks.push(chunk);
}

// 2. 识别共享模块
let shared_modules = self.find_shared_modules();

// 3. 为共享模块创建 vendor chunk
for (module_id, import_count) in shared_modules {
if import_count >= config.min_shared_count {
// 创建独立的 vendor chunk
let vendor_chunk = self.create_vendor_chunk(module_id);
self.chunks.push(vendor_chunk);
}
}

// 4. 应用大小限制
self.split_large_chunks(config.max_chunk_size);
}

// 查找被多个模块共享的依赖
fn find_shared_modules(&self) -> HashMap<ModuleId, usize> {
let mut shared_count = HashMap::new();

for module in self.module_graph.modules.values() {
for dep in &module.dependencies {
*shared_count.entry(*dep).or_insert(0) += 1;
}
}

// 过滤出被多次引用的模块
shared_count
.into_iter()
.filter(|(_, count)| *count > 1)
.collect()
}

// 分割过大的 chunks
fn split_large_chunks(&mut self, max_size: usize) {
let mut new_chunks = Vec::new();

for chunk in &mut self.chunks {
if chunk.size > max_size {
// 按模块大小排序
chunk.modules.sort_by_key(|m| {
self.module_graph.modules[m].size
});

// 分割成多个小 chunks
let mut current_chunk = Chunk::new(chunk.id);
let mut current_size = 0;

for module_id in &chunk.modules {
let module_size =
self.module_graph.modules[module_id].size;

if current_size + module_size > max_size {
// 创建新 chunk
new_chunks.push(current_chunk);
current_chunk = Chunk::new(ChunkId::new());
current_size = 0;
}

current_chunk.modules.push(*module_id);
current_size += module_size;
}

new_chunks.push(current_chunk);
}
}

self.chunks.extend(new_chunks);
}
}

并行编译

Rolldown 充分利用 Rust 的并发特性,实现多阶段并行编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
use rayon::prelude::*;

pub async fn parallel_build(
module_graph: &ModuleGraph
) -> Result<Vec<Chunk>> {
// 阶段 1:并行解析所有模块
let modules: Vec<_> = module_graph
.modules
.par_iter() // 并行迭代器
.map(|(id, module)| {
parse_and_transform(module)
})
.collect();

// 阶段 2:并行 Tree-shaking
let shaken_modules: Vec<_> = modules
.par_iter()
.map(|module| {
tree_shake(module)
})
.collect();

// 阶段 3:并行生成 chunks
let chunks: Vec<_> = shaken_modules
.par_chunks(100) // 分批处理
.map(|batch| {
generate_chunk(batch)
})
.collect();

Ok(chunks)
}

内存优化

Rolldown 使用多种技术减少内存占用:

零拷贝字符串:

1
2
3
4
5
6
7
8
9
// 使用 Cow (Clone on Write) 避免不必要的字符串复制
use std::borrow::Cow;

pub struct Module<'a> {
// 只在需要修改时才复制
source: Cow<'a, str>,
// 使用引用而非拥有
imports: Vec<&'a Import>,
}

Arena 分配器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 使用 arena 分配器减少内存碎片
use bumpalo::Bump;

pub struct Parser<'a> {
allocator: &'a Bump,
}

impl<'a> Parser<'a> {
pub fn parse(&self, source: &'a str) -> &'a Program {
// 所有 AST 节点在同一块内存中分配
let program = self.allocator.alloc(Program::new());
// ...
program
}
}

关键特性:

与 Rollup 的兼容性

Rolldown 提供与 Rollup 兼容的 API 和插件接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
build: {
rollupOptions: {
// Rolldown 支持大部分 Rollup 配置
output: {
manualChunks: {
'vendor': ['react', 'react-dom'],
},
},
plugins: [
// 大部分 Rollup 插件可直接使用
],
},
},
});

高级代码分割:advancedChunks

Rolldown 引入了比 Rollup 更强大的代码分割配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export default defineConfig({
build: {
rollupOptions: {
output: {
advancedChunks: {
groups: [
{
name: 'react-vendor',
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
priority: 10,
},
{
name: 'ui-components',
test: /[\\/]src[\\/]components[\\/]/,
minSize: 20000, // 最小 20KB
priority: 5,
},
],
minSize: 10000,
maxSize: 500000,
},
},
},
},
});

Hook 过滤优化

Rolldown 通过 Hook 过滤 减少 Rust 和 JavaScript 运行时之间的通信开销:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default {
plugins: [
{
name: 'my-plugin',
// 只对特定文件触发 hook
resolveId: {
filter: /\.custom$/,
handler(id) {
// 只有 .custom 文件才会调用这个 hook
return { id: id + '.resolved' };
},
},
},
],
};

性能基准测试

根据官方测试数据,Rolldown 在各个维度都有显著提升:

项目 规模 Rollup Rolldown 提升倍数 内存减少
GitLab 大型 45s 17s 2.6x 100x
Excalidraw 中型 8s 0.5s 16x 50x
Vue 3 中型 12s 4s 3x 30x
React Admin 大型 38s 11s 3.5x 60x

性能提升来源:

  1. Rust 原生性能:相比 JavaScript,Rust 编译后的机器码执行效率高 10-100 倍
  2. 并行处理:充分利用多核 CPU,解析、转换、压缩全部并行化
  3. 零拷贝优化:减少内存分配和数据复制
  4. 增量编译:HMR 时只重新处理变化的模块
  5. 统一 AST:OXC 提供的统一 AST 避免重复解析

实际项目对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 大型 React 项目(2000+ 组件)
# Vite 6 (Rollup)
$ time npm run build
real 1m 23s
user 2m 15s
sys 0m 8s
Memory: 1.2GB

# Vite 8 (Rolldown)
$ time npm run build
real 0m 18s
user 0m 45s
sys 0m 3s
Memory: 180MB

# 提升:4.6x 速度,6.7x 内存

迁移到 Rolldown

使用 rolldown-vite(推荐)

rolldown-vite 是 Vite 7 的 Rolldown 版本,作为过渡方案:

1
2
3
4
5
# 安装 rolldown-vite
npm install rolldown-vite --save-dev

# 或使用 pnpm
pnpm add -D rolldown-vite
1
2
3
4
5
6
7
8
9
// vite.config.js
import { defineConfig } from 'rolldown-vite';

export default defineConfig({
// 配置与 Vite 7 完全相同
plugins: [
// 现有插件大多可直接使用
],
});

等待 Vite 8 正式版

Vite 8 将默认使用 Rolldown,届时可直接升级。

OXC 工具集合

Rust 驱动的编译器基础设施

什么是 OXC?

OXC(Oxidation Compiler)是由字节跳动团队开发的高性能 JavaScript/TypeScript 工具链,旨在提供统一的编译器基础设施。

OXC 核心组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
graph TB
A[源代码] --> B[OXC Parser]
B --> C[统一 AST]
C --> D1[OXC Linter]
C --> D2[OXC Transformer]
C --> D3[OXC Minifier]
C --> D4[OXC Resolver]
C --> D5[OXC Formatter]

D1 --> E1[Lint 结果]
D2 --> E2[转换后的代码]
D3 --> E3[压缩后的代码]
D4 --> E4[模块路径]
D5 --> E5[格式化的代码]

style C fill:#f96

OXC Parser

  • 支持:ES2024、TypeScript、JSX、Flow
  • 性能:比 SWC 快 3 倍
  • 特性:零拷贝、增量解析
1
2
3
4
5
6
7
// OXC Parser 内部实现(简化)
pub struct Parser<'a> {
source: &'a str,
allocator: &'a Allocator,
// 零拷贝的 token 流
tokens: Vec<Token<'a>>,
}

OXC Transformer

  • 功能:TypeScript 类型擦除、JSX 转换、语法降级
  • 性能:比 Babel 快 40-70 倍
1
2
3
4
5
6
7
8
9
10
11
// 使用 OXC Transformer
import { transform } from '@oxc-transform/core';

const result = transform(code, {
jsx: {
runtime: 'automatic', // React 17+ 自动 JSX
},
typescript: {
onlyRemoveTypeImports: true,
},
});

OXC Minifier

  • 性能:比 SWC 快 8 倍,比 esbuild 快 50%
  • 特性:保留语义的激进优化
1
2
3
4
5
6
// Rolldown 中使用 OXC Minifier
export default defineConfig({
build: {
minify: 'oxc', // 使用 OXC 压缩器
},
});

OXC Linter

  • 性能:比 ESLint 快 50-100 倍
  • 兼容性:支持大部分 ESLint 规则
1
2
3
4
5
# 安装 oxlint
npm install oxlint --save-dev

# 运行
npx oxlint src/
1
2
3
4
5
6
7
8
// .oxlintrc.json
{
"rules": {
"no-unused-vars": "error",
"no-console": "warn"
},
"extends": ["eslint:recommended"]
}

OXC Resolver

  • 性能:比 webpack 快 28 倍
  • 功能:支持 exportsimports、条件导出

统一 AST 的价值

OXC 的核心优势在于统一 AST

1
2
3
4
5
6
7
8
9
graph LR
A[源代码] --> B[OXC Parser]
B --> C[统一 AST]
C --> D[Linter]
C --> E[Transformer]
C --> F[Minifier]
C --> G[Bundler]

style C fill:#f66

好处:

  1. 零序列化开销:各工具共享同一份 AST,无需重复解析
  2. 内存效率:单次解析,多次使用
  3. 一致性:所有工具看到的代码结构完全一致

OXC 在 Rolldown 中的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Rolldown 内部流程(简化)
async function bundle(entry) {
// 1. 使用 OXC Parser 解析
const ast = oxc.parse(code);

// 2. 使用 OXC Transformer 转换
const transformed = oxc.transform(ast, options);

// 3. 构建模块图
const graph = buildModuleGraph(transformed);

// 4. Tree-shaking
const shaken = treeShake(graph);

// 5. 使用 OXC Minifier 压缩
const minified = oxc.minify(shaken);

return minified;
}

Vitest

与 Vite 深度集成的测试框架

为什么选择 Vitest?

传统测试框架(如 Jest)存在以下问题:

  • ❌ 需要单独的配置文件
  • ❌ 不支持 ESM(或支持不完善)
  • ❌ 启动慢(需要预编译)
  • ❌ 与 Vite 配置不一致

Vitest 的优势:

  • ✅ 复用 Vite 配置和插件
  • ✅ 原生 ESM 支持
  • ✅ 极速启动(利用 Vite 的 HMR)
  • ✅ 兼容 Jest API

核心架构

1
2
3
4
5
6
7
8
9
10
11
12
13
graph TB
A[测试文件] --> B[Vitest Runner]
B --> C[Vite Dev Server]
C --> D[模块图]
D --> E[HMR]
E --> F[增量测试]

B --> G[测试上下文]
G --> H[断言]
G --> I[Mock]
G --> J[覆盖率]

style C fill:#9f9

快速开始

安装

1
npm install -D vitest

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
test: {
// 测试环境
environment: 'jsdom', // 或 'node', 'happy-dom'

// 全局 API(可选)
globals: true,

// 覆盖率
coverage: {
provider: 'v8', // 或 'istanbul'
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'dist/',
'**/*.spec.ts',
],
},

// 浏览器模式(实验性)
browser: {
enabled: false,
name: 'chrome', // 或 'firefox', 'webkit'
},
},
});

编写测试

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/utils/sum.test.ts
import { describe, it, expect } from 'vitest';
import { sum } from './sum';

describe('sum', () => {
it('should add two numbers', () => {
expect(sum(1, 2)).toBe(3);
});

it('should handle negative numbers', () => {
expect(sum(-1, -2)).toBe(-3);
});
});

运行测试

1
2
3
4
5
6
7
8
9
10
11
# 运行所有测试
npx vitest

# 监听模式(默认)
npx vitest --watch

# 单次运行
npx vitest run

# 覆盖率
npx vitest --coverage

高级特性

Workspace 模式

对于 Monorepo 项目:

1
2
3
4
5
6
7
8
9
10
// vitest.workspace.js
export default [
'packages/*/vitest.config.ts',
{
test: {
name: 'integration',
include: ['tests/integration/**/*.test.ts'],
},
},
];

浏览器模式

1
2
3
4
5
6
7
8
9
10
11
// vite.config.js
export default defineConfig({
test: {
browser: {
enabled: true,
name: 'chrome',
headless: true,
provider: 'playwright', // 或 'webdriverio'
},
},
});
1
2
3
4
5
6
7
8
// 浏览器 API 测试
import { test, expect } from 'vitest';

test('should render button', async () => {
document.body.innerHTML = '<button>Click me</button>';
const button = document.querySelector('button');
expect(button?.textContent).toBe('Click me');
});

快照测试

1
2
3
4
5
6
7
8
9
10
11
import { test, expect } from 'vitest';

test('should match snapshot', () => {
const data = {
name: 'John',
age: 30,
hobbies: ['reading', 'coding'],
};

expect(data).toMatchSnapshot();
});

Mock 和 Spy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { vi, test, expect } from 'vitest';

test('should mock fetch', async () => {
// Mock 全局 fetch
global.fetch = vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ data: 'mocked' }),
})
);

const response = await fetch('/api/data');
const data = await response.json();

expect(data).toEqual({ data: 'mocked' });
expect(fetch).toHaveBeenCalledWith('/api/data');
});

性能对比

项目规模 Jest Vitest 提升
小型(50 测试) 3.2s 0.8s 4x
中型(500 测试) 18s 6.3s 2.9x
大型(2000 测试) 65s 22s 3x

内存占用:

  • Jest:~400MB
  • Vitest:~240MB(减少 40%

迁移到新版 Vite 生态

迁移前准备

  • [ ] 确认 Node.js 版本 >= 18
  • [ ] 备份现有配置文件
  • [ ] 检查插件兼容性
  • [ ] 运行现有测试套件

Rolldown 迁移

  • [ ] 安装 rolldown-vite
  • [ ] 更新 vite.config.js
  • [ ] 测试开发环境
  • [ ] 测试生产构建
  • [ ] 验证插件功能

OXC 工具迁移

  • [ ] 替换 ESLint 为 oxlint(可选)
  • [ ] 配置 OXC Minifier
  • [ ] 测试代码压缩效果

Vitest 迁移

  • [ ] 安装 Vitest
  • [ ] 迁移 Jest 配置
  • [ ] 更新测试脚本
  • [ ] 运行测试验证
  • [ ] 配置 CI/CD

完整配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
plugins: [react()],

// Rolldown 配置
build: {
// 使用 OXC 压缩器
minify: 'oxc',

rollupOptions: {
output: {
// 高级代码分割
advancedChunks: {
groups: [
{
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
priority: 10,
},
],
},
},
},
},

// Vitest 配置
test: {
globals: true,
environment: 'jsdom',
setupFiles: './tests/setup.ts',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
include: ['src/**/*.{ts,tsx}'],
exclude: [
'src/**/*.test.{ts,tsx}',
'src/**/*.spec.{ts,tsx}',
],
},
},
});

package.json 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest",
"test:ui": "vitest --ui",
"test:run": "vitest run",
"test:coverage": "vitest --coverage",
"lint": "oxlint src/"
},
"devDependencies": {
"vite": "^6.0.0",
"vitest": "^2.0.0",
"oxlint": "^0.10.0",
"@vitest/ui": "^2.0.0",
"@vitest/coverage-v8": "^2.0.0"
}
}

CI/CD 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# .github/workflows/ci.yml
name: CI

on: [push, pull_request]

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Lint
run: npm run lint

- name: Test
run: npm run test:run

- name: Coverage
run: npm run test:coverage

- name: Build
run: npm run build

- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage/coverage-final.json

性能对比与最佳实践

代码分割策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
export default defineConfig({
build: {
rollupOptions: {
output: {
advancedChunks: {
groups: [
// 1. 框架代码(变化最少)
{
name: 'framework',
test: /[\\/]node_modules[\\/](react|react-dom|vue)[\\/]/,
priority: 20,
},
// 2. UI 库(变化较少)
{
name: 'ui',
test: /[\\/]node_modules[\\/](antd|@mui)[\\/]/,
priority: 15,
},
// 3. 业务组件(变化较多)
{
name: 'components',
test: /[\\/]src[\\/]components[\\/]/,
minSize: 20000,
priority: 10,
},
// 4. 工具函数
{
name: 'utils',
test: /[\\/]src[\\/]utils[\\/]/,
minSize: 10000,
priority: 5,
},
],
},
},
},
},
});

测试优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// vitest.config.js
export default defineConfig({
test: {
// 1. 使用线程池
pool: 'threads',
poolOptions: {
threads: {
singleThread: false,
maxThreads: 4,
},
},

// 2. 智能监听
watchExclude: [
'**/node_modules/**',
'**/dist/**',
'**/.git/**',
],

// 3. 测试隔离
isolate: true,

// 4. 覆盖率优化
coverage: {
provider: 'v8', // v8 比 istanbul 快 2-3 倍
reportsDirectory: './coverage',
clean: true,
},
},
});

Lint 优化

1
2
3
4
5
# 使用 oxlint 替代 ESLint(可选)
npx oxlint src/ --fix

# 或保留 ESLint,但只检查必要的规则
npx eslint src/ --cache --max-warnings 0

总结与展望

核心要点

  1. Rolldown 统一了开发和生产环境,解决了双轨架构的不一致问题
  2. OXC 提供了高性能的编译器基础设施,显著提升了构建速度
  3. Vitest 与 Vite 深度集成,提供了极速的测试体验

技术栈对比

工具 旧方案 新方案 核心优势
打包器 esbuild + Rollup Rolldown 统一、快 3-16 倍
解析器 esbuild OXC Parser 快 3 倍
转换器 esbuild/Babel OXC Transformer 快 40-70 倍
压缩器 esbuild/Terser OXC Minifier 快 8 倍
Linter ESLint oxlint 快 50-100 倍
测试框架 Jest Vitest 快 3-4 倍

参考资料

官方文档

GitHub 仓库

技术文章

性能基准