Skip to content

Improving Performance

Profile First

The Duration line of the summary breaks the run down into phases, as percentages of all tracked time:

Duration  3.76s (environment 79%, import 13%, transform 6%, tests 1%, setup 1%)

The percentages are relative to the sum of all tracked phases, not to the wall-clock time: phases run in parallel workers, so their sum is usually larger than the run itself. In a multi-project setup the percentages aggregate over all projects, so a phase that dominates one project can be diluted by the others; the performance hints below analyze each project separately.

The phases map to configuration options:

  • environment - creating the test environment (for example jsdom, happy-dom) for test files. See Test Environments.
  • transform - waiting for Vite to resolve and transform imported modules. See Caching Between Reruns.
  • import - evaluating test files and their modules, excluding the transform wait tracked above. When files import mostly the same modules (typical for barrel-file imports), isolation re-evaluates that shared graph for every file. See Test Isolation.
  • setup - running setupFiles.
  • worker - preparing the test runner in each worker. Isolation pays this cost for every test file. See Test Isolation.
  • tests - running the tests themselves. A run dominated by this phase has little to gain from configuration changes.

When the collected timings show that a configuration change would make the run significantly faster, Vitest also prints a hint after the summary, see experimental.diagnostics. Hints never suggest changing an option that was set explicitly.

vitest doctor measures the alternative configurations instead of estimating them: it runs the suite under each candidate and reports the comparison, including whether the tests pass with isolate: false.

Test Isolation

By default Vitest runs every test file in an isolated environment based on the pool:

  • threads pool runs every test file in a separate Worker
  • forks pool runs every test file in a separate forked child process
  • vmThreads pool runs every test file in a separate VM context, but it uses workers for parallelism

This greatly increases test times, which might not be desirable for projects that don't rely on side effects and properly cleanup their state (which is usually true for projects with node environment). In this case disabling isolation will improve the speed of your tests. To do that, you can provide --no-isolate flag to the CLI or set test.isolate property in the config to false.

bash
vitest --no-isolate
ts
import { defineConfig } from 'vitest/config'

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

You can also disable isolation for specific files only by using projects:

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

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

TIP

If you are using vmThreads pool, you cannot disable isolation. Use threads pool instead to improve your tests performance.

For some projects, it might also be desirable to disable parallelism to improve startup time. To do that, provide --no-file-parallelism flag to the CLI or set test.fileParallelism property in the config to false.

bash
vitest --no-file-parallelism
ts
import { defineConfig } from 'vitest/config'

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

Test Environments

DOM environments are expensive to create: jsdom costs roughly 200-500ms per import and happy-dom roughly 90-200ms, plus the time to construct the window. With an isolating pool (the default), that cost is paid for every test file, because every file gets a fresh worker. On DOM-heavy suites this is often the largest cost of the run; it appears as the environment share of the Duration breakdown.

Three configurations reduce this cost:

configurationenvironment createdisolationtrade-off
pool: 'forks'/'threads' + isolate: true (default)once per filefresh process/thread and environment per filesafest, slowest
pool: 'vmThreads'once per workerfresh VM context and window per filetest code runs in a VM realm: cross-realm instanceof edge cases with externalized packages, and memory is not reclaimed as reliably (see vmMemoryLimit)
isolate: falseonce per workernone - files in the same worker share the environment and module statetests must not depend on a clean window or module state; run vitest doctor to check
vitest.config.js
ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    environment: 'jsdom',
    pool: 'vmThreads', // environment per worker, fresh window per file
  },
})

Prefer isolate: false with threads if the tests tolerate shared state: it is the fastest option and keeps memory behavior simple. Use vmThreads when every file needs a fresh window and the per-file environment cost dominates the run. happy-dom is cheaper to create than jsdom in every setup.

You can limit the working directory when Vitest searches for files using test.dir option. This should make the search faster if you have unrelated folders and files in the root directory.

Caching Between Reruns

In watch mode, Vitest caches all transformed files in memory, which makes reruns fast. However, this cache is discarded once the test run finishes. By enabling fsModuleCache, Vitest persists this cache to the file system so it can be reused across reruns.

This improvement is most noticeable when rerunning a small number of tests that depend on a large module graph. For full test suites, parallelization already mitigates the cost because other tests populate the in-memory cache while earlier tests are still running. For example, running one test file with a huge module graph (>900 modules):

shell
# the first run
Duration  8.75s (import 43%, transform 32%, tests 20%, setup 5%)

# the second run
Duration  5.90s (tests 44%, import 35%, transform 13%, setup 8%)

Node Compile Cache

Vitest supports Node's on-disk compile cache: when the NODE_COMPILE_CACHE environment variable points at a directory, the V8 bytecode of Vitest's own modules and of your externalized dependencies is written to disk and reused by later runs instead of being recompiled. Vitest propagates the variable to every worker, and workers persist the modules they compiled when they shut down.

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

The first run with an empty directory pays for serializing the compiled modules, so this is only worth enabling when the directory survives between runs: local runs, or CI pipelines that cache the directory. NODE_DISABLE_COMPILE_CACHE=1 disables the cache entirely, taking precedence over NODE_COMPILE_CACHE.

Note that Vitest automatically disables the compile cache in workers when the v8 coverage provider is enabled — V8 serializes cached scripts without the source positions that precise coverage relies on.

Pool

By default Vitest runs tests in pool: 'forks'. While 'forks' pool is better for compatibility issues (hanging process and segfaults), it may be slightly slower than pool: 'threads' in larger projects.

You can try to improve test run time by switching pool option in configuration:

bash
vitest --pool=threads
ts
import { defineConfig } from 'vitest/config'

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

Sharding

Test sharding is a process of splitting your test suite into groups, or shards. This can be useful when you have a large test suite and multiple machines that could run subsets of that suite simultaneously.

To split Vitest tests on multiple different runs, use --shard option with --reporter=blob option:

sh
vitest run --reporter=blob --shard=1/3 # 1st machine
vitest run --reporter=blob --shard=2/3 # 2nd machine
vitest run --reporter=blob --shard=3/3 # 3rd machine

Vitest splits your test files, not your test cases, into shards. If you've got 1000 test files, the --shard=1/4 option will run 250 test files, no matter how many test cases individual files have.

Collect the results stored in .vitest/blob/ directory from each machine and merge them with --merge-reports option:

sh
vitest run --merge-reports

When running the same shards across multiple environments, set the VITEST_BLOB_LABEL environment variable so merged reports can display them separately:

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

This setup is also used at https://github.com/vitest-tests/test-sharding.

yaml
# Inspired from 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

If your tests create file-based attachments (for example via context.annotate or custom artifacts), upload and restore attachmentsDir in the merge job as shown above.

TIP

Test sharding can also become useful on high CPU-count machines.

Vitest will run only a single Vite server in its main thread. Rest of the threads are used to run test files. In a high CPU-count machine the main thread can become a bottleneck as it cannot handle all the requests coming from the threads. For example in 32 CPU machine the main thread is responsible to handle load coming from 31 test threads.

To reduce the load from main thread's Vite server you can use test sharding. The load can be balanced on multiple Vite server.

sh
# Example for splitting tests on 32 CPU to 4 shards.
# As each process needs 1 main thread, there's 7 threads for test runners (1+7)*4 = 32
# Use 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