Skip to content

Vitest 5.0

SeungAh Hong10min read

Vitest 5.0: Performance, Trace View, and the Migration Guide

Vitest 5.0 shipped on September 3, 2026 (first tag: 5.0.1). Unlike Vite 8, this major isn't an engine swap — it's a cleanup that moves defaults toward the safer side and consolidates scattered artifacts and options into one place.

Which splits how you feel it:

  • The new features are easy to see — Trace View, vi.when, nested projects
  • The breakage is not — the clearMocks default, unawaited async assertions, the -t separator, artifact paths

This post leans on the latter. Release notes tell you what got better; what you need on upgrade day is a list of what quietly changes color.

📋 Table of Contents

  1. Requirements and Changes at a Glance
  2. Performance
  3. New Features
  4. Migration Guide
  5. The Quiet Breakage
  6. Applying It to This Repo
  7. Wrapping Up

Requirements and Changes at a Glance

ItemRequired version
Node.js>= 22.12.0
Vite>= 6.4.0 (peer dependency)
Vitest tag5.0.1 (2026-09-03)

Vite becoming a peer dependency is the first gate. npm, pnpm, Bun, and Deno install it automatically; Yarn users must install it themselves.

# Yarn only
yarn add -D vite

At a glance:

CategoryWhat changed
PerformanceVM pools up to 53% · isolated suites up to 25% · Browser Mode 16–18% faster
New featuresTrace View · nested projects · vi.when · benchmark rewrite · Temporal mocking
Default flipsclearMocks: true · strict locators · inline projects inherit config · shared Vite server
StricterUnawaited resolves/rejects fail · expect.poll rejects on timeout
CleanupAll artifacts under .vitest/ · old entry points removed · test.sequential gone

Performance

For this release the Vitest team built a new benchmark suite measuring apps from 5 to 1,280 modules across pool and environment combinations — an attempt to say "it got faster" along axes instead of by feel.

AxisImprovement
VM pools53% faster on dependency-heavy apps
Isolated suitesUp to 25% faster on large suites
Browser Mode16–18% faster in Chrome

Three sources account for most of it:

  • Inline projects share a Vite server — no more transforming the same file once per project
  • fsModuleCache — transformed modules are cached on the file system and persist across runs. Worth the most on repeated local runs and CI that warms a cache
  • Fewer worker round trips, plus a smaller install through bundled dependencies

The reporter's duration breakdown now shows percentages — small, but it tells you at a glance whether transform or collect is the slow part.

One caveat: these numbers depend on pool, environment, and module count. For a small unit suite on jsdom, 53% is somebody else's story — you'll feel install size and caching first.

New Features

1. Trace View (Browser Mode)

The headline feature. Browser Mode can record every interaction and assertion as DOM snapshots, letting you replay a test step by step after it finishes.

export default defineConfig({
  test: {
    browser: {
      traceView: true,
    },
  },
});

The value is concentrated in failures. Browser tests are hard to debug because the screen at the moment of failure is already gone — Trace View brings it back. That's aimed squarely at the "broke once in CI, won't reproduce locally" category.

2. Nested Projects and Config Inheritance

A monorepo-facing change. Two things moved together:

  • Inline projects inherit the root config by default (extends now defaults to true)
  • Referenced config files can declare their own projects, so projects form a hierarchy

Filtering got shorter too:

vitest -p app        # short for --project app

3. vi.when — Conditional Mocking per Argument

Returning different values per argument is common, and until now it meant branching inside mockImplementation. vi.when makes it declarative.

import { vi, expect } from 'vitest';
 
const fetchUser = vi.fn();
 
vi.when(fetchUser)
  .calledWith(1)
  .thenReturn({ id: 1, name: 'Ada' })
  .calledWith(expect.any(Number))
  .thenReturn(null);
  • Arguments are compared by deep equality
  • Asymmetric matchers like expect.any() work in argument positions
  • A toHaveBeenExhausted assertion ships alongside, for checking that queued responses were all consumed

4. Benchmark API Rewrite

bench moved from a top-level import to a test fixture.

// Before (Vitest 4)
import { bench } from 'vitest';
 
bench('sort', () => {
  data.slice().sort();
});
// After (Vitest 5)
import { test } from 'vitest';
 
test('sort', async ({ bench }) => {
  await bench(() => {
    data.slice().sort();
  });
});

The reasoning is consistent: once a benchmark is a test, it inherits fixtures, hooks, retries, and filtering for free. Benchmarks lived in their own world; this folds them back into the main one. Results now flow through the default and json reporters instead of a separate path.

The cost is a lot of removals.

RemovedReplacement
bench.skip · bench.onlytest's skip/only
benchmark.reporters · benchmark.comparedefault/json reporters
benchmark.outputFile · benchmark.outputJson.vitest/ artifact paths
--compare · --outputJson CLI flags

5. Locators and Temporal

  • Strict locator matching by defaultlocator.exact starts enabled
  • ARIA tree in error messages — pick 'html' | 'aria' | 'all' via browser.locators.errorFormat
  • Temporal API mockingvi.useFakeTimers() / vi.setSystemTime() now freeze Temporal alongside Date, so Temporal.Now follows the fake clock

6. Other Config and API Additions

ItemWhat it does
--repeatsRe-runs the same test to hunt flaky tests
coverage.autoAttachSubprocessTracks coverage through child processes
coverage.thresholds.perFileNow accepts an object
sharedViteServerControls Vite server sharing for inline projects
TestCase.logs()Reporters can read console output
Custom matchersCan access the underlying Chai assertion object
HTML reporter singleFile: trueSelf-contained single file — ideal as a CI artifact

Migration Guide

Step 1: Runtime and Vite

node -v          # must be >= 22.12.0

If you're pinned to the Node 20 line, you're blocked right here. That makes this a runtime upgrade schedule, not a Vitest upgrade.

pnpm add -D vitest@5
# On Yarn, install vite explicitly
yarn add -D vitest@5 vite

Step 2: clearMocks Now Defaults to On

The change most likely to alter your tests quietly. Mock history is now cleared before each test.

// Used to pass
it('a', () => {
  doWork();
});
it('b', () => {
  expect(spy).toHaveBeenCalledTimes(2); // counting calls that leaked from 'a'
});

The direction is right — it stops mock leakage between tests. But tests that relied on that leakage now fail. You can revert:

export default defineConfig({
  test: { clearMocks: false },
});

The better move is to fix rather than revert. Turn the flag off and you go on never knowing which tests depend on their neighbors' state.

Step 3: Unawaited Async Assertions

// Before: printed a warning, passed
expect(promise).resolves.toBe(1);
 
// After: fails the test
await expect(promise).resolves.toBe(1);

A warning promoted to a failure. If you'd been ignoring that warning, you had assertions that verified nothing. Most of the new red after upgrading marks a spot that was already empty.

expect.poll got strict in the same direction — it now rejects if the callback or assertion doesn't settle within the timeout. The callback receives an AbortSignal for cancellation.

Step 4: Hoisted Mocking Calls Must Be Top-Level

// After: throws
describe('suite', () => {
  vi.mock('./api'); // ❌ inside a block
});
 
// Correct placement
vi.mock('./api'); // ✅ file top level
describe('suite', () => {});

Calling vi.mock, vi.unmock, or vi.hoisted inside a function, block, or callback now throws. These were always hoisted and executed at the top level, so this just makes where they're written match when they actually run.

Step 5: The -t Separator Changed

# Full test names are joined with ' > '
vitest -t 'math > adds'    # ✅
vitest -t 'math adds'      # ❌ no longer matches

Patterns confined to a single segment — a tag like -t '@smoke' — are unaffected. What breaks are patterns that spanned describe and test names. If -t is baked into CI scripts, audit them against that rule.

Step 6: Removed and Relocated APIs

// test.sequential / describe.sequential removed
test.sequential('name', fn); // ❌
test('name', { concurrent: false }, fn); // ✅

Entry points were consolidated:

BeforeAfter
vitest/coveragevitest/node
vitest/reportersvitest/node
vitest/environmentsvitest/runtime
vitest/snapshotvitest/runtime
vitest/runners · vitest/suite · vitest/mockerremoved entirely

At the package level: @vitest/runner and @vitest/ws-client are deprecated, @vitest/expect no longer shares state with Vitest's expect, and @vitest/browser-webdriverio moved to the vitest-community org.

Step 7: Artifact Paths Moved

Scattered output is now all under .vitest/. Update .gitignore and CI artifact upload paths along with it.

KindBeforeAfter
Attachments.vitest-attachements/.vitest/attachments/
Screenshots__screenshots__/.vitest/attachments/failure-screenshots/
Blob report.vitest-reports/.vitest/blob/
HTMLhtml/index.html.vitest/index.html (option is outputDir)
JSONstdout.vitest/json/output.json
JUnitstdout.vitest/junit/output.xml

Watch JSON and JUnit moving from stdout to files in particular. A CI step that piped and parsed that output will now quietly receive empty input.

The Quiet Breakage

From the rest of the migration guide, here are the items whose error message won't tell you the cause.

ChangeHow it shows up
Coverage include/excludePatterns match relative to project root and the "contains" behavior is gone. Patterns without wildcards are treated as directories. If coverage numbers swing for no reason, look here
No parent-directory lookupVitest no longer searches parent directories for a config. Pass --config explicitly and use --dir for test discovery
toThrow('')An empty string is a substring of every string, so it matches any error message. Use /^$/ to target empty messages
toHaveTextContentNow a strict equality check. For partial/regex matching, use the new toMatchTextContent()
1-based worker IDsVITEST_POOL_ID and VITEST_WORKER_ID now start at 1, not 0. Setups that shard databases or ports by that value are off by one
jsdom global assignmentAssignments to globalThis properties propagate to the underlying DOM implementation
populateGlobalThe originals map holds property descriptors, not values. Restore with Object.defineProperty()
Class mock prototypesClass mock instances now inherit from the implementation's prototype — methods resolve and instanceof works (so branches relying on the old behavior flip)
Browser automocksBrowser Mode now applies automocks correctly. Automocked exports return undefined by default. To run the real implementation while tracking calls, use { spy: true }
resolveConfig returnNo longer returns a { vitestConfig, viteConfig } pair. It returns the resolved Vite config; test config lives on .test
Vitest UI token authhttp://localhost:51204/__vitest__/?token=... — your bookmarked URL won't open
Browser session bindingRunner URLs require a sessionId. Use the URLs Vitest prints

Two more if you use Browser Mode: locators passed to custom commands are now SerializedLocator objects with selector and locator fields, and render() is async in vitest-browser-vue and vitest-browser-svelte, so it must be awaited. browser.api and browser.isolate are deprecated in favor of top-level api and isolate.

Applying It to This Repo

Reading alone leaves nothing behind, so I measured this blog's repo against the list. Current state:

// package.json
"engines": { "node": ">=20.9.0" },
"devDependencies": { "vitest": "^2.1.8" }
// vitest.config.ts
test: {
  environment: 'jsdom',
  globals: true,
  setupFiles: ['./vitest.setup.ts'],
}
ItemVerdictWhy
Node 22.12+⚠️engines says >=20.9.0. This has to move before Vitest does
Vitest 2 → 5⚠️Skipping two majors — Vitest 4's changes need reading too
-t '@smoke'The tag in the test:smoke script sits inside one segment, so the separator change doesn't touch it
clearMocks⚠️Not set in config, so the default flip lands as-is — needs a real run
jsdom global assignment⚠️globals: true + jsdom + vitest.setup.ts puts it in range of the propagation change
Every Browser Mode itemNot used. E2E is Playwright's job here
Artifact pathsNo JSON/JUnit reporters, so no impact

Which narrows the actual work to three lines:

  1. Raising engines to Node 22.12+ is the precondition — without it the rest isn't a discussion
  2. Take the clearMocks default as-is rather than disabling it — if tests break, that's information
  3. Skip every Browser Mode item — the list looks long, but the number of gates this repo actually has to pass is small

Item 3 is worth writing down. Read all 34 migration entries top to bottom and every one looks like your problem, when in reality you hit as many as the features you turned on. If you don't use Browser Mode, benchmarks, or custom reporters, more than half of them were never yours.

Wrapping Up

Vitest 5 is a release where the defaults, not the features, are the story. clearMocks turns on, locators get strict, warnings become failures, and scattered artifacts collapse into one directory. Every one of those points the same direction — making things that used to pass ambiguously stop passing.

So more red after upgrading is usually not a bad sign. Assertions that verified nothing because they weren't awaited; counts that included a neighbor's mock calls — spots that were already empty are finally visible. The temptation to flip clearMocks: false is strongest right here, and that flag doesn't fix the problem; it hides it again.

Upgrade checklist:

  • ✅ Confirm Node.js 22.12+ and Vite 6.4+ (on Yarn, install vite yourself)
  • clearMocks on by default — fix the failing tests instead of disabling it
  • ✅ Audit every unawaited resolves/rejects
  • ✅ Move vi.mock / vi.hoisted to the file top level
  • ✅ Check that CI -t patterns don't span segments
  • test.sequential{ concurrent: false }
  • ✅ Move artifact paths to .vitest/.gitignore, artifact uploads, and any stdout-parsing step
  • ✅ Re-check coverage include/exclude patterns (root-relative, wildcard-less means directory)
  • ✅ Make setups reading VITEST_POOL_ID / VITEST_WORKER_ID 1-based
  • ✅ On Browser Mode: strict locators · async render() · SerializedLocator · browser.apiapi

References