Vitest Slow in CI? Fix Startup, Imports and Pools
When Vitest is slow, the cause is almost always one of four things: environment creation, slow imports, test isolation overhead, or too few workers in CI. This guide covers why Vitest is slow on startup and in CI, how to fix slow imports, how to find slow tests, and how to shard. It is a sibling of Jest slow and part of the slow e2e tests cluster.
Vitest changes option names between major versions, especially around pools. Where a name matters, check the configuration reference for your version.
Profile first: the Duration line
Vitest's summary line breaks down where time went:
Duration 3.76s (environment 79%, import 13%, transform 6%, tests 1%, setup 1%)
The percentages are shares of tracked time across all workers, not of wall-clock. Read it as a diagnosis:
- environment high: creating
jsdomorhappy-domfor every file. Fix: usenodewhere possible. - import / transform high: slow imports or transforms. Fix: see below.
- tests high: the tests themselves are slow. Fix the tests.
- setup / worker high:
setupFilesor isolation overhead per file.
Vitest slow startup
Startup cost is paid per run and, with isolation, per test file. To reduce it:
- Use the
nodeenvironment by default and opt intojsdomper file with// @vitest-environment jsdom. - Keep
setupFileslight. Heavy setup (starting servers, loading fixtures) belongs inglobalSetup, which runs once. - Run
vitest runin CI rather than watch mode, and cachenode_moduleswith the setup-node cache. See GitHub Actions caching.
Vitest slow imports
Vitest evaluates every imported module in each isolated test file. If many files import the same large graph, each pays for it again. The most common cause is barrel files: import { x } from "@/components" pulls in every component.
// slow: loads the whole barrel
import { Button } from "@/components";
// fast: loads one module
import { Button } from "@/components/Button";
Other fixes: mock heavy modules (vi.mock) that the test does not need, avoid importing whole icon or utility libraries, and check the import breakdown in the Vitest UI where your version provides it. If a file imports 2,000 modules to test one function, the import is the test's real cost.
Pool and isolation settings
Vitest runs test files in worker pools (threads or forked processes). The choice and the isolation setting trade speed for safety:
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
pool: "threads", // or "forks" (safer with native modules)
isolate: false, // reuse the module graph between files; faster, riskier
},
});
npx vitest run --pool=threads
npx vitest run --pool=forks
forksis more compatible with native addons and process-level APIs;threadscan be quicker to start and lighter on memory.isolate: falsestops re-evaluating shared imports for each file, which can cut import and worker time a lot. The risk is shared state leaking between files, so only enable it if your tests do not mutate module-level state or globals. Newer versions also allow per-file or per-project isolation settings.- Worker count follows available cores by default. On a 2-vCPU runner you will have one or two workers, which is why CI feels slow. Set the maximum workers option for your version if you have more cores or less memory.
Change one setting at a time and compare the Duration line.
Vitest: find slow tests
Use the verbose reporter to see durations, and the slowTestThreshold option to flag slow tests (default 300 ms):
test: { slowTestThreshold: 500 }
npx vitest run --reporter=verbose
npx vitest run --reporter=json --outputFile=report.json
Sort the JSON by duration to get your top 20. Often the slowest are tests with real timers, real network or real databases; use fake timers (vi.useFakeTimers()) and mocks, or move them to a separate integration project.
Vitest slow in CI specifically
Beyond the settings above:
- Fewer cores: CI runners are smaller than laptops. Compare locally with the same worker count.
- Cold caches: cache dependencies; Vite's transform cache lives in
node_modules/.vite, and persisting it can help reruns on the same code. - Coverage: coverage instrumentation can make runs much slower. Run coverage in one job, not in every shard.
- Type checking: do not run
typecheckmode inside the main unit test job; runtsc --noEmitin its own job.
Shard Vitest across jobs
Vitest supports --shard:
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: npm }
- run: npm ci
- run: npx vitest run --shard=${{ matrix.shard }}/3 --reporter=blob
Blob reports from shards can be merged with vitest --merge-reports, for combined output and coverage. Details on balancing and cost are in the test sharding guide, and the planner suggests a shard count.
More cores
Once settings are tuned, suites that spread across files scale with cores. See faster GitHub Actions runners for the options. compiler.dev was faster than GitHub's 2-core runner on 15 of 19 benchmarked stacks, by 1.1 to 3 times, and comparison mode measures your own jobs. For the whole pipeline, start at the slow CI hub.
FAQ
Why is Vitest slow on startup?
Usually environment creation (jsdom), heavy setupFiles, or a large import graph repeated per file. Read the Duration line to see which.
Why is Vitest slow in CI but fast locally?
CI has fewer cores, a cold cache, and may run coverage or type checks that you skip locally.
How do I find slow tests in Vitest?
Use --reporter=verbose and slowTestThreshold, or write JSON output and sort by duration.
Is it safe to set isolate to false?
Only if tests do not share mutable module state. It is a big speed win when safe; test it on your suite and watch for order-dependent failures.
Made by compiler.dev. Free tools · Pricing