单靠默认并行无法解决千万行级项目测试耗时问题,必须组合优化线程池、分片、依赖加载和隔离策略;maxworkers设为6或8并关闭fileparallelism,启用deps.optimizer,按实际耗时而非文件数分片,避免vmthreads内存泄漏。

直接结论:单靠默认并行无法解决千万行级项目的测试耗时问题,必须组合调整线程池、分片、依赖加载和隔离策略,否则容易陷入 CPU 饱和但主线程阻塞的假并行状态。
为什么 maxWorkers: '1.5x' 在超大项目里反而拖慢速度
千万行级项目通常伴随大量模块解析和依赖图构建,maxWorkers 只控制 worker 数量,不缓解主线程压力。Vitest 的主线程仍需串行处理:vitest.config.resolve_include_glob、import 分析、测试收集等阶段。实测发现当 maxWorkers > 8 且文件数 > 5000 时,主线程 CPU 占用常达 95%+,worker 大量空转。
- 优先把
maxWorkers固定为6或8,而非自动倍率 - 配合
fileParallelism: false关闭文件级并行,让主线程集中做一次导入分析,避免重复解析同一桶文件(如src/index.ts) - 用
npx vitest run --reporter=import-breakdown确认前 3 个耗时最高的导入路径,针对性改写为深层路径导入(例如从import { a } from 'pkg'改为import { a } from 'pkg/dist/a')
pool: 'vmThreads' 在大型项目中的内存泄漏风险
vmThreads 启动快、通信快,但每个 VM 上下文不会自动释放全局变量或定时器。千万行项目中若存在未清理的 setInterval、process.on('uncaughtException') 或第三方库缓存(如 lodash.memoize),会导致 worker 进程内存持续增长,最终触发 OOM 或 GC 频繁卡顿。
- 生产环境禁用
vmThreads;开发阶段可临时启用,但必须配poolOptions.vmThreads.maxHeapSize: '1g' - 更稳妥的选择是
pool: 'threads'+poolOptions.threads.singleThread: false,它复用线程但不共享 VM 上下文,内存可控性更好 - 所有
beforeAll/afterAll必须显式清理副作用,尤其注意jest.useFakeTimers()类 API 的配对调用
分片不是“加机器就变快”,关键在 --shard 的负载均衡
直接写 --shard=1/3 并不能保证三台机器负载均等。Vitest 按测试文件数量平均切分,但一个含 200 个 it 的文件可能比 50 个文件总耗时还长。结果常出现某一分片跑 8 分钟,另两个 2 分钟就结束。
- 先运行
npx vitest run --reporter=json > test-stats.json,提取每个文件的duration字段 - 用脚本按总耗时排序,手动将长耗时文件(> 3s)单独划入不同 shard,再把剩余短文件按累计时间均衡分配
- CI 中不要硬编码
--shard=1/3,改用动态参数:vitest run --shard=$SHARD_INDEX/$SHARD_TOTAL,由调度层控制分配逻辑
最容易被忽略的瓶颈:deps.optimizer 没开或配置错
千万行项目往往依赖数十个大型包(React、Lodash、Zod、SWR 等),默认情况下 Vitest 每次都重新解析它们的 ESM 输出,这部分可占总启动时间 40%+。而 deps.optimizer 能把它们预构建为本地缓存的 optimized chunk。
- 必须显式启用:
deps: { optimizer: { web: { enabled: true } } } -
include列表要写具体包名,不能只写'*';推荐从node_modules中ls -S | head -20找出体积最大的包填入 - 首次运行会慢(因为要构建),但后续所有
vitest run都会跳过这些包的解析,实测提升 2–5 倍冷启动速度
真正卡住千万行项目测试速度的,从来不是 worker 数量,而是主线程在反复解析同一堆依赖、测试文件分布严重不均、以及 optimizer 开关没打开。调优顺序应该是:先跑一遍 import-breakdown 定位导入热点,再开 deps.optimizer,最后才动 pool 和 shard。否则所有并行配置都在给瓶颈让路。











