第23章 Vitest 测试性能分析指南:耗时指标解析与 Profiling 配置
第23章:测试性能分析
运行 Vitest 时,它会报告多项测试耗时指标:
```bash RUN v2.1.1 /x/vitest/examples/profiling
✓ test/prime-number.test.ts (1) 4517ms ✓ generate prime number 4517ms
Test Files 1 passed (1) Tests 1 passed (1) Start at 09:32:53 Duration 4.80s (transform 44ms, setup 0ms, import 35ms, tests 4.52s, environment 0ms) ```
耗时指标解析
- Transform(转换):文件转换所消耗的时间。详见“文件转换”部分。 - Setup(初始化):执行 setupFiles 文件所消耗的时间。 - Import(导入):导入测试文件及其依赖项所消耗的时间。这也包括收集所有测试用例的耗时。注意,此指标不包含测试内部的动态导入(dynamic imports)。 - Tests(测试执行):实际运行测试用例所消耗的时间。 - Environment(环境):搭建测试环境(例如 JSDOM)所消耗的时间。
测试运行器
如果测试执行耗时过高,可以生成测试运行器的性能分析数据(profile)。请参考 NodeJS 文档了解以下选项: - `--cpu-prof` - `--heap-prof` - `--prof`
警告:由于 `node:worker_threads` 的限制,`--prof` 选项无法在 `pool: 'threads'` 模式下正常工作。
要在 Vitest 的测试运行器中传递这些选项,需在 Vitest 配置中定义 `execArgv`:
```ts import { defineConfig } from 'vitest/config'
export default defineConfig({ test: { fileParallelism: false, execArgv: [ '--cpu-prof', '--cpu-prof-dir=test-runner-profile', '--heap-prof', '--heap-prof-dir=test-runner-profile' ], }, }) ```
测试运行结束后,应已生成 `test-runner-profile/*.cpuprofile` 和 `test-runner-profile/*.heapprofile` 文件。请参考“查看性能分析记录”了解如何分析这些文件。
更多示例请参考 Profiling | Examples。
主线程
分析主线程的性能对于调试 Vitest 中 Vite 的使用情况以及 globalSetup 文件非常有用。Vite 插件也是在此处运行的。
提示:关于 Vite 专属的性能分析技巧,请参考 Performance | Vite。
我们推荐使用 `vite-plugin-inspect` 来分析 Vite 插件的性能。
为此,需要向运行 Vitest 的 Node 进程传递参数:
```bash $ node --cpu-prof --cpu-prof-dir=main-profile ./node_modules/vitest/vitest.mjs --run # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^ # NodeJS 参数 Vitest 参数 ```
测试运行结束后,应已生成 `main-profile/*.cpuprofile` 文件。请参考“查看性能分析记录”了解如何分析这些文件。
文件转换
这种性能分析策略是识别由桶文件(barrel files)引起的非必要转换的好方法。如果日志中包含测试运行时本不应加载的文件,说明可能存在不必要的桶文件导入。
你也可以使用 Vitest UI 来调试由桶文件引起的慢速问题。下例展示了不使用桶文件导入文件如何将转换的文件数量减少约 85%。
文件树示例 ```text example.test.ts ├── src │ └── utils │ ├── currency.ts │ ├── formatters.ts <-- 待测文件 │ ├── index.ts │ ├── location.ts │ ├── math.ts │ ├── time.ts │ └── users.ts ├── test │ └── formatters.test.ts └── vitest.config.ts ```
```ts import { expect, test } from 'vitest' // 通过桶文件导入,会拉入所有 util import { formatter } from '../src/utils' // 直接导入具体文件,避免拉入无关代码 import { formatter } from '../src/utils/formatters'
test('formatter works', () => { expect(formatter).not.toThrow() }) ```
要在 UI 中查看文件是如何转换的,可以打开“Module Info”视图。
文件导入
某些模块加载时间较长。要找出最慢的模块,请在配置中启用 `experimental.importDurations`:
vitest.config.ts ```ts import { defineConfig } from 'vitest/config'
export default defineConfig({ test: { experimental: { importDurations: { print: true, }, }, }, }) ```
测试结束后,这将打印出最慢导入项的分解报告:
```bash Import Duration Breakdown (Top 10)
Module Self Total my-test.test.ts 5ms 620ms [████████████████████] date-fns/index.js 500ms 500ms [████████████████░░░░] src/utils/helpers.ts 10ms 120ms [████████░░░░░░░░░░░░] ```
你也可以不改配置,直接通过 CLI 使用 `--experimental.importDurations.print`:
```bash vitest --experimental.importDurations.print ```
识别出慢速模块后,有几种策略可以加速导入:
使用特定入口点
许多库提供多个入口点。导入主入口点(通常也是桶文件)往往会拉入比你需要的多得多代码。
例如,`date-fns` 从主入口点重新导出了数百个函数。不要从顶层模块导入,而是直接从具体函数模块导入:
```ts // 慢:导入所有功能 import { format } from 'date-fns' // 快:只导入 format import { format } from 'date-fns/format' ```
使用 resolve.alias 重定向导入
如果依赖项没有提供细粒度的入口点,或者第三方代码导入了沉重的入口点,可以使用 `resolve.alias` 将导入重定向到更轻量的替代方案:
vitest.config.ts ```ts import { defineConfig } from 'vitest/config'
export default defineConfig({ resolve: { alias: [ { find: /^date-fns$/, replacement: join(dirname(require.resolve('date-fns/package.json')), 'index.cjs'), }, ] }, }) ```
使用依赖项优化器
Vitest 可以使用 `deps.optimizer` 将外部库打包成单个文件,从而减少导入包含许多内部模块的包时的开销:
vitest.config.ts ```ts import { defineConfig } from 'vitest/config'
export default defineConfig({ test: { deps: { optimizer: { ssr: { enabled: true, include: ['date-fns'], }, }, }, }, }) ```
这对 UI 库和具有深层导入树的包特别有效。在 node/edge 环境中使用 `optimizer.ssr`,在 jsdom/happy-dom 环境中使用 `optimizer.client`。
代码覆盖率
如果项目中的代码覆盖率生成速度很慢,可以使用 `DEBUG=vitest:coverage` 环境变量启用性能日志记录。
```bash $ DEBUG=vitest:coverage vitest --run --coverage
RUN v3.1.1 /x/vitest-example
vitest:coverage Reading coverage results 2/2 vitest:coverage Converting 1/2 vitest:coverage 4 ms /x/src/multiply.ts vitest:coverage Converting 2/2 vitest:coverage 552 ms /x/src/add.ts vitest:coverage Uncovered files 1/2 vitest:coverage File "/x/src/large-file.ts" is taking longer than 3s vitest:coverage 3027 ms /x/src/large-file.ts vitest:coverage Uncovered files 2/2 vitest:coverage 4 ms /x/src/untested-file.ts vitest:coverage Generate coverage total time 3521 ms ```
这种性能分析方法非常适合检测被覆盖率提供商意外选取的大文件。例如,如果你的配置意外地将大型构建后的压缩 JavaScript 文件包含在代码覆盖率中,它们应该会出现在日志中。在这种情况下,你可能需要调整 `coverage.include` 和 `coverage.exclude` 选项。
查看性能分析记录
你可以使用各种工具查看 `*.cpuprofile` 和 `*.heapprofile` 的内容。以下是一些示例工具:
- Speedscope - Visual Studio Code 中的 JavaScript 性能分析 - 使用 Performance 面板分析 Node.js 性能(developer.chrome.com) - Memory 面板概览(developer.chrome.com)