Vitest 5.0 is out!
September 4th, 2026

The next Vitest major is here
Today, we are thrilled to announce Vitest 5!
Quick links:
- Docs
- Translations: 简体中文
- Migration Guide
- GitHub Changelog
If you've not used Vitest before, we suggest reading the Getting Started and Features guides first.
We extend our gratitude to the over 790 contributors to Vitest Core and to the maintainers and contributors of Vitest integrations, tools, and translations who have helped us develop this new major release. We encourage you to get involved and help us improve Vitest for the entire ecosystem. Learn more at our Contributing Guide.
To get started, we suggest helping triage issues, review PRs, send failing tests PRs based on open issues, and support others in Discussions and Vitest Land's help forum. If you'd like to talk to us, join our Discord community and say hi on the #contributing channel.
For the latest news about the Vitest ecosystem and Vitest core, follow us on Bluesky or Mastodon.
To stay updated, keep an eye on the VoidZero blog and subscribe to the newsletter.
Performance Improvements
Performance was the main focus of this release. To measure it, we built vitest-dev/benchmarks: a set of generated reference apps, from a 5-file utility package to an enterprise monolith with 1,280 modules and a barrel-file-heavy app with 817 modules. Every app runs across pools (forks, threads, vmForks, vmThreads), environments (node, jsdom, happy-dom, and Browser Mode), with and without isolation, so we can see how each change behaves on realistic projects instead of micro-benchmarks.
Here is a selection of cells from the comparison between Vitest 4.1.10 and Vitest 5.0 (Apple M4, 10 cores, Node 24, whole-process wall clock of vitest run, median of 3 runs):
| App | Configuration | Vitest 4.1 | Vitest 5.0 | Change |
|---|---|---|---|---|
| micro-utils (5 test files) | vmThreads, jsdom | 0.61s | 0.56s | −8% |
| node-library (40 test files) | forks, isolated | 0.86s | 0.75s | −13% |
| deps-heavy | vmThreads | 1.59s | 0.74s | −53% |
| react-spa (92 modules) | vmThreads, jsdom | 1.25s | 1.07s | −15% |
| react-spa (92 modules) | Browser Mode, Chromium | 2.40s | 2.01s | −16% |
| vue-spa (37 components) | Browser Mode, Chromium | 1.94s | 1.58s | −18% |
| design-system (80 components) | vmThreads, jsdom | 2.09s | 1.72s | −18% |
| barrel-hell (817 modules) | forks, isolated, fsModuleCache | 1.33s | 1.08s | −18% |
| enterprise-monolith (1,280 modules) | forks, isolated | 7.24s | 5.83s | −19% |
long-haul (80 jsdom files) | vmForks, happy-dom | 5.43s | 4.06s | −25% |
| cpu-bound (30 test files) | threads, 100% workers | 0.91s | 0.83s | −8% |
The biggest wins are in the vm pools, in Browser Mode, and in large isolated suites. Cells that were already dominated by the environment setup, like forks with jsdom and isolation, stay within ±3% of Vitest 4.1. The full result set for every cell is in the benchmarks repository.
Some of the changes behind these numbers:
- Inline projects share the Vite server. Projects defined in
test.projectsthat don't change the Vite config now reuse the Vite server of the config that declares them, so shared files are transformed once. SeesharedViteServer. - File system module cache is stable. The
fsModuleCacheoption (previouslyexperimental.fsModuleCache) persists transformed modules on disk, so they are reused across reruns and separate Vitest processes. Plugins can participate in the cache key withdefineCacheKeyGenerator. - Fewer round trips between the main process and workers. Warm modules are served to workers in one round trip.
- Faster vm pools.
vmThreadsandvmForksreuse compiled code across contexts and prewarm the module graph. They also supportrequire(esm)now. - Faster Browser Mode. Vitest prebundles its own runtime, prewarms the browser while the Vite server starts, opens browser sessions adaptively instead of
maxWorkerssessions upfront, and cuts per-file round trips. - Smaller install. Vitest now bundles its own dependencies, which reduces the number of packages in
node_modulesand the time spent resolving them. - Faster coverage. The
v8provider merges reports with bounded memory and precompiled globs, both providers send less data over RPC, andistanbulmoved to the maintained@vitest/istanbuljspackages. The coverage tables in the benchmarks repository list every app.
The duration breakdown in the reporter output now shows percentages, so it's easier to see where the time goes:
Duration 3.76s (environment 79%, import 13%, transform 6%, tests 1%, setup 1%)Trace View
Vitest 5 adds a built-in Trace View for Browser Mode. When browser.traceView is enabled, Vitest records every interaction, assertion, and page.mark as a DOM snapshot and lets you replay the test step by step after the browser has already moved on. The viewer is available in the browser UI, in Vitest UI, and in the HTML reporter, so it works for local debugging and for CI failures.
Select a step to see the reconstructed page at that moment with the interacted element highlighted, and Vitest opens the source location in the editor panel. Failed actions and assertions are highlighted in red. Trace view also supports keyboard navigation and live updates in watch mode.
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
browser: {
traceView: true,
},
},
})vitest --browser.traceViewUnlike Playwright Traces, trace view does not depend on the provider and does not require a separate viewer.
Nested Projects and Config Inheritance
Inline projects now inherit the root config by default, including Vite options like plugins and resolve.alias. In Vitest 4, you had to set extends: true on every project to get this behavior:
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
projects: [
{
extends: true,
test: {
name: 'unit',
include: ['**/*.unit.test.ts'],
},
},
],
},
})A config file referenced in test.projects can now declare its own projects. Such a config acts as a container, exactly like the root config, and provides nested projects named app (unit), app (e2e), and so on. This makes it possible to reference a package that already defines its own projects without duplicating them at the root:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: ['./packages/*/vitest.config.ts'],
},
})The --project filter is aware of the hierarchy, and it now has a -p shorthand:
vitest -p appvi.when
Defining different return values for different arguments used to require a manual mockImplementation with argument checks. The new vi.when API defines per-argument behaviors on a spy. Arguments are matched with deep equality and support asymmetric matchers like expect.any():
import { expect, test, vi } from 'vitest'
test('returns user data', async () => {
const findById = vi.fn()
vi.when(findById)
.calledWith(1)
.thenResolve({ id: 1, name: 'Ella' })
.calledWith(2)
.thenResolve({ id: 2, name: 'Gracie' })
.calledWith(expect.any(Number))
.thenReject(new Error('not found'))
await expect(findById(1)).resolves.toEqual({ id: 1, name: 'Ella' })
await expect(findById(3)).rejects.toThrow('not found')
})Behaviors can be limited with thenReturnOnce or a times option, and the new toHaveBeenExhausted assertion checks that every registered behavior was consumed. Read more in the Conditional Mocking recipe.
Benchmarking Rewrite
The benchmarking API was rewritten. bench is no longer a top-level import; it is a test-context fixture available inside regular test() calls in benchmark files. This gives benchmarks access to everything the test runner offers: fixtures, lifecycle hooks, retries, filtering, and assertions.
import { expect, test } from 'vitest'
test('compare parsers', async ({ bench }) => {
const result = await bench.compare(
bench('JSON.parse', () => {
JSON.parse('{"key":"value"}')
}),
bench('custom parser', () => {
customParse('{"key":"value"}')
}),
)
expect(result.get('JSON.parse')).toBeFasterThan(result.get('custom parser'))
})Results can be stored with writeResult and replayed with bench.from() to compare against a baseline, and the built-in Tinybench provider can be replaced with a custom benchmark provider. Benchmark output is now part of the default and json reporters. See the Benchmarking guide for the full API.
Locator Errors Show the ARIA Tree
When a locator cannot find an element in Browser Mode, Vitest now prints the ARIA snapshot of the searched subtree next to the HTML output. The accessibility tree is usually much shorter than the raw HTML and shows exactly the roles and names that getByRole and getByLabelText match against. The output is controlled by the new browser.locators.errorFormat option:
export default defineConfig({
test: {
browser: {
locators: {
errorFormat: 'aria', // 'html' | 'aria' | 'all'
},
},
},
})Locators are also strict by default: locators.exact is enabled, so getByText('Item') no longer matches Item 1 by accident.
Mocking Temporal
Fake timers now mock the Temporal API alongside Date, thanks to the @sinonjs/fake-timers v15.4 update. This applies both to vi.useFakeTimers() and to vi.setSystemTime() used without fake timers:
vi.setSystemTime(0)
Temporal.Now.instant().epochMilliseconds // 0Temporal is part of the default set of faked APIs. To avoid faking it, add it to toNotFake in the config or when invoking vi.setSystemTime().
Stricter Assertions
Asynchronous assertions like resolves, rejects, and toMatchFileSnapshot now fail the test when they are not awaited. Before, Vitest awaited them at the end of the test and only printed a warning. The test still passed even though the assertion never ran at the point where it was written.
test('unawaited assertion', async () => {
expect(promise).resolves.toBe(1)
await expect(promise).resolves.toBe(1)
})expect.poll now rejects when it does not settle within timeout, and the callback receives an AbortSignal so you can cancel in-flight work:
await expect.poll(async ({ signal }) => {
const response = await fetch('/api/status', { signal })
return response.status
}, { timeout: 1000 }).toBe(200)Assertion types now expose both the return type and the received type. When you extend matchers, the Matchers interface now takes the return type as its first parameter:
import 'vitest'
declare module 'vitest' {
interface Matchers<T = any> {
toBeFoo: () => void
}
interface Matchers<R, T> {
toBeFoo: () => R
}
}R reflects how the matcher is used: void when called synchronously, Promise<void> through .resolves, .rejects, expect.poll, or expect.element. T is the type of the received value, so an expected argument can be typed the same as the value under test:
declare module 'vitest' {
interface Matchers<R, T> {
toEqualTyped: (expected: T) => R
}
}
expect(1).toEqualTyped(2) // ✅
expect(1).toEqualTyped('2') // ❌ type errorThe same change applies to code that refers to assertion types directly:
Assertion<string>
Assertion<void, string>
Assertion<Promise<void>, string> // asynchronous assertionCustom matchers also get access to the underlying Chai assertion object.
clearMocks is Enabled by Default
clearMocks now defaults to true. Vitest calls vi.clearAllMocks() before every test, so a mock no longer carries call history from one test into the next while the implementations stay intact. This removes one of the most common sources of order-dependent tests. To keep the previous behavior, set clearMocks: false.
Reporters Updates
Reporters and other integrations now write their output into a single .vitest directory at the project root: the html, json, and junit reporters, failure screenshots, and new traces all use it by default. This reduces the number of entries you need to add to .gitignore to one.
Third-party reporters can use the same convention through the new vitest.createReport(scope) API, which returns a Report limited to its own .vitest/<scope> directory.
The HTML reporter can also produce a self-contained report with the singleFile option. Vitest inlines the UI assets, metadata, and test attachments into one index.html, which is easy to upload as a CI artifact:
export default defineConfig({
test: {
reporters: [
['html', { singleFile: true }],
],
},
})Other Improvements
- The new
--repeatsCLI option repeats every test a given number of times regardless of the result, which is useful for hunting flaky tests. injectCjsGlobalsmakes it possible to disable the injection ofmodule,exports,require,__filename, and__dirnameinto ES modules.coverage.autoAttachSubprocesstracks the coverage ofnode:child_processandnode:worker_threadsspawned during the test run with thev8provider.coverage.thresholds.perFileaccepts an object, andthresholds.autoUpdatereceives the previous threshold as an argument.- The
jsonreporter accepts afilterMetaoption, and thejunitreporter supports jest-junit-compatible naming options. TestCase.logs()exposes the console output recorded during a test to reporters and the advanced API.- Test titles and inspected values use
pretty-format, andtest.for/test.eachtitle placeholders support non-ASCII characters. vitest --merge-reportssupports non-sharded runs across multiple environments.- Coverage switched to the
@vitest/istanbuljspackages, a maintained fork of theistanbul-lib-*family.
Breaking Changes
Vitest 5 requires Vite >= 6.4.0 and Node.js >= 22.12.0. Vitest 5 has several breaking changes that could affect you, so we advise reviewing the detailed Migration Guide before upgrading.
The complete list of changes is at the Vitest 5 Changelog.
Acknowledgments
Vitest 5 is the result of countless hours of work by the Vitest team and our contributors. None of it would be possible without the individuals and companies that sponsor Vitest. Vladimir and Hiroshi work on Vite and Vitest full-time at VoidZero, and Chromatic gives Ari the time to keep pushing Vitest forward. A big thank you to everyone supporting us through GitHub Sponsors and Open Collective.