进阶 vitest.dev 2026-10-22 19:00:41 · 18 阅读

第24章 提升性能

提升性能

先剖析 摘要中的 Duration 一行将运行过程拆分为若干阶段,并以占所有跟踪时间的百分比形式展示:

Duration 3.76s(环境 79%,导入 13%,转换 6%,测试 1%,初始化 1%)

百分比是相对于所有跟踪阶段总时间的比例,而非墙钟时间:因为阶段在并行工作者中执行,其总时长通常长于单次运行本身。在多项目配置中,百分比是跨所有项目聚合的,因此某个项目的支配性阶段可能被其他项目稀释;下文提到的性能建议会单独分析每个项目。

这些阶段对应不同的配置项:

环境 — 为测试文件创建测试环境(例如 jsdom、happy-dom)。参见 测试环境。 转换 — 等待 Vite 解析并转换导入的模块。参见 重新运行之间的缓存。 导入 — 求值测试文件及其模块,排除上文跟踪的转换等待。当文件导入大量相同的模块时(桶文件导入的典型情况),隔离机制会为每个文件重新求值该共享依赖图。参见 测试隔离。 初始化 — 运行 setupFiles。 工作者 — 在每个工作者中准备测试运行器。隔离机制会为每个测试文件支付此成本。参见 测试隔离。 测试 — 运行测试本身。如果运行时间主要由该阶段主导,配置变更带来的收益有限。

当采集到的计时数据显示某项配置变更能显著加快运行时,Vitest 还会在摘要后打印提示,参见 experimental.diagnostics。提示不会建议修改已显式设置的选项。

vitest doctor 会测量替代配置的效果而非估算:它在每个候选配置下运行测试套件并报告对比结果,包括在 isolate: false 下测试是否通过。

测试隔离 默认情况下,Vitest 基于池在隔离环境中运行每个测试文件: 线程池(threads pool)为每个测试文件使用独立的工作者(Worker) 分叉池(forks pool)为每个测试文件使用独立的分叉子进程 VM 线程池(vmThreads pool)为每个测试文件使用独立的 VM 上下文,但使用工作者实现并行

这会大幅增加测试耗时,对于不依赖副作用且能正确清理状态的项目(通常适用于使用 Node 环境的项目)而言,这可能并非理想。此时禁用隔离可提升测试速度。做法是为 CLI 提供 --no-isolate 标志,或在配置中将 test.isolate 属性设为 false。

CLI: bash vitest --no-isolate

vitest.config.js: ts import { defineConfig } from 'vitest/config'

export default defineConfig({ test: { isolate: false, }, })

你还可以通过使用 projects 仅为特定文件禁用隔离:

vitest.config.js: ts import { defineConfig } from 'vitest/config'

export default defineConfig({ test: { projects: [ { test: { name: 'Isolated', isolate: true, // (默认值) exclude: ['.non-isolated.test.ts'], }, }, { test: { name: 'Non-isolated', isolate: false, include: ['.non-isolated.test.ts'], }, }, ], }, })

提示:如果你使用的是 vmThreads 池,则无法禁用隔离。请改用 threads 池以提升测试性能。

在某些项目中,为改善启动时间也可能需要禁用并行性。做法是为 CLI 提供 --no-file-parallelism 标志,或在配置中将 test.fileParallelism 属性设为 false。

CLI: bash vitest --no-file-parallelism

vitest.config.js: ts import { defineConfig } from 'vitest/config'

export default defineConfig({ test: { fileParallelism: false, }, })

测试环境 创建 DOM 环境代价高昂:jsdom 每次导入成本约 200-500ms,happy-dom 约 90-200ms,外加构建 window 对象的时间。在使用隔离池(默认)时,由于每个文件都会获得一个新工作者,这项成本会为每个测试文件支付。在 DOM 密集型测试套件中,这往往是运行中最大的开销,体现在 Duration 分解中的环境占比。

以下三种配置可降低此成本:

配置 | 环境创建时机 | 隔离 | 权衡 池设为 'forks'/'threads' 且 isolate: true(默认) | 每个文件一次 | 每个文件使用全新进程/线程及环境 | 最安全,最慢 池设为 'vmThreads' | 每个工作者一次 | 每个文件使用全新 VM 上下文及 window | 测试代码运行在 VM 领域:与外部化包可能存在跨领域 instanceof 边界情况,且内存回收不如可靠(参见 vmMemoryLimit) isolate: false | 每个工作者一次 | 无 — 同一工作者的文件共享环境及模块状态 | 测试不能依赖干净的 window 或模块状态;运行 vitest doctor 进行检查

vitest.config.js: ts import { defineConfig } from 'vitest/config'

export default defineConfig({ test: { environment: 'jsdom', pool: 'vmThreads', // 每个工作者创建一个环境,每个文件刷新 window }, })

如果测试容忍共享状态,优先使用 threads 池配合 isolate: false:这是最快的选项,且内存行为简单。当每个文件都需要新 window 且单文件环境成本主导运行时间时,使用 vmThreads。在任何设置下,happy-dom 的创建成本都低于 jsdom。

限制目录搜索 可以使用 test.dir 选项限制 Vitest 搜索文件时的工作目录。如果根目录包含无关的文件夹和文件,这将使搜索更快。

重新运行之间的缓存 在监听模式下,Vitest 在内存中缓存所有已转换的文件,从而加快重新运行。但测试运行结束后,此缓存会被丢弃。启用 fsModuleCache(默认开启)后,Vitest 会将此缓存持久化到文件系统,以便在重新运行时复用。

当重新运行少量依赖庞大模块图的测试时,此改进最为明显。对于完整测试套件,并行化已减轻成本,因为在早期测试仍在运行时,其他测试已填充内存缓存。例如,运行一个拥有巨大模块图(超过 900 个模块)的测试文件:

shell # 第一次运行 Duration 8.75s(导入 43%,转换 32%,测试 20%,初始化 5%)

# 第二次运行 Duration 5.90s(测试 44%,导入 35%,转换 13%,初始化 8%)

Node 编译缓存 Vitest 支持 Node 的磁盘编译缓存:当 NODE_COMPILE_CACHE 环境变量指向某个目录时,Vitest 自身模块及外部依赖的 V8 字节码会被写入磁盘,并在后续运行中复用,而非重新编译。Vitest 会将该变量传播到每个工作者,工作者在关闭时持久化其已编译的模块。

shell NODE_COMPILE_CACHE=node_modules/.cache/node-compile-cache vitest

在空目录下首次运行时需支付序列化已编译模块的成本,因此仅当目录在运行之间存续时启用此功能才有益:本地运行,或缓存该目录的 CI 流水线。NODE_DISABLE_COMPILE_CACHE=1 会完全禁用缓存,其优先级高于 NODE_COMPILE_CACHE。

注意,当启用 v8 覆盖率提供者时,Vitest 会自动在工作者中禁用编译缓存 — V8 序列化缓存脚本时不包含精确覆盖率所依赖的源码位置信息。

池 默认情况下 Vitest 在池设为 'forks' 时运行测试。虽然 'forks' 池在兼容性问题(挂起进程和段错误)方面更优,但在大型项目中可能略慢于池设为 'threads'。

你可以尝试通过修改配置中的 pool 选项来改善测试运行时间:

CLI: bash vitest --pool=threads

vitest.config.js: ts import { defineConfig } from 'vitest/config'

export default defineConfig({ test: { pool: 'threads', }, })

分片 测试分片是将测试套件划分为组(或分片)的过程。当测试套件庞大且有多台机器可以同时运行该套件的不同子集时,这非常有用。

要在不同的运行中分割 Vitest 测试,请使用带有 --reporter=blob 的 --shard 选项:

sh vitest run --reporter=blob --shard=1/3 # 第一台机器 vitest run --reporter=blob --shard=2/3 # 第二台机器 vitest run --reporter=blob --shard=3/3 # 第三台机器

Vitest 将测试文件而非测试用例分片。如果你有 1000 个测试文件,--shard=1/4 选项将运行 250 个测试文件,无论每个文件包含多少测试用例。

从每台机器的 .vitest/blob/ 目录收集结果,并使用 --merge-reports 选项进行合并:

sh vitest run --merge-reports

在多个环境中运行相同分片时,请设置 VITEST_BLOB_LABEL 环境变量,以便合并后的报告能单独显示它们:

sh VITEST_BLOB_LABEL=linux vitest run --reporter=blob --shard=1/3

GitHub Actions 示例

此设置也用于 https://github.com/vitest-tests/test-sharding.yaml

灵感来自 https://playwright.dev/docs/test-sharding

name: Tests on: push: branches: - main jobs: tests: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest] shardIndex: [1, 2, 3, 4] shardTotal: [4] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24

- name: Install pnpm uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0

- name: Install dependencies run: pnpm i

- name: Run tests run: pnpm run test --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} env: VITEST_BLOB_LABEL: ${{ matrix.os }}

- name: Upload Vitest results GitHub Actions Artifacts if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: vitest-results-${{ matrix.os }}-${{ matrix.shardIndex }} path: .vitest include-hidden-files: true retention-days: 1

merge-reports: if: ${{ !cancelled() }} needs: [tests]

runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24

- name: Install pnpm uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0

- name: Install dependencies run: pnpm i

- name: Download Vitest results from GitHub Actions Artifacts uses: actions/download-artifact@v4 with: path: .vitest merge-multiple: true

- name: Merge reports run: npx vitest --merge-reports

如果你的测试创建基于文件的附件(例如通过 context.annotate 或自定义工件),请在合并任务中如上文所示上传和恢复 attachmentsDir。

提示:测试分片在高 CPU 核数的机器上也能发挥作用。

Vitest 仅在其主线程中运行单个 Vite 服务器。其余线程用于运行测试文件。在高 CPU 核数的机器上,主线程可能成为瓶颈,因为它无法处理来自所有线程的请求。例如在 32 CPU 机器上,主线程需处理来自 31 个测试线程的负载。

为减轻主线程 Vite 服务器的负载,可以使用测试分片。负载可平衡到多个 Vite 服务器上。

sh # 示例:将测试在 32 CPU 上分割为 4 个分片。 # 每个进程需要 1 个主线程,因此测试运行器有 7 个线程 (1+7)*4 = 32 # 使用 VITEST_MAX_WORKERS: VITEST_MAX_WORKERS=7 vitest run --reporter=blob --shard=1/4 & \ VITEST_MAX_WORKERS=7 vitest run --reporter=blob --shard=2/4 & \ VITEST_MAX_WORKERS=7 vitest run --reporter=blob --shard=3/4 & \ VITEST_MAX_WORKERS=7 vitest run --reporter=blob --shard=4/4 & \ wait # https://man7.org/linux/man-pages/man2/waitpid.2.html

vitest run --merge-reports

评论 (0)