Vitest 5.0.0 landed on September 3, 2026, and most of the coverage since has been about what breaks. Fair enough; we wrote that piece too, and if you haven't upgraded yet, start with the Vitest 5 migration guide, because nothing below matters while your install fails on Node 20. This piece is the other half: the new capabilities, ranked by payoff, once the suite is green again. I've been running 5.0 on a mid-sized TypeScript monorepo since release week, and a few of these features earned their place fast. Others I'd leave alone for now.
Quick take: The features worth immediate adoption are
vi.whenfor per-argument mocks,fsModuleCachefor faster reruns, and Trace View if you run Browser Mode. The performance work needs no adoption at all: the Vitest team's public benchmarks show 8 to 25 percent faster runs across configurations, up to 53 percent on vm pools, just for upgrading. The benchmark API rewrite is the one to postpone unless you already ship perf-sensitive library code.
The Speed Is Free, and the Numbers Are Published
Performance headlines from tool vendors deserve suspicion. What makes these different is that the methodology is public: the team measured against vitest-dev/benchmarks, a suite of real project shapes ranging from a 5-file utility library to a 1,280-module monolith, across pools and environments. From the announcement post:
| Scenario | Vitest 4 | Vitest 5 | Change |
|---|---|---|---|
| deps-heavy project, vm pool | 1.59s | 0.74s | -53% |
| enterprise-monolith, isolated | 7.24s | 5.83s | -19% |
| React/Vue SPA, Browser Mode | baseline | faster | -16 to -18% |
The spread across all configurations is 8 to 25 percent. Where will your suite land? Somewhere in that band, depending on pool and isolation settings, and the new vitest doctor command will tell you exactly where: it reruns your suite under alternative configurations and prints the options that would make it faster. That's a genuinely new idea for a test runner. Configuration advice has always been folklore passed between blog posts; here it's measured against your own code.
Two config options do the durable work. fsModuleCache, promoted from experimental.fsModuleCache, persists transformed modules to disk so reruns skip transformation entirely. And sharedViteServer lets projects in test.projects that don't alter the Vite config reuse one server instead of each booting their own, which is exactly the shape most monorepo test setups have.
vi.when Deletes Your Ugliest Mock Code
Every TypeScript codebase with service mocks has that one mockImplementation callback with a switch statement inside. Mine had several. The new vi.when API replaces the pattern with per-argument stubbing:
import { vi, test, expect } from 'vitest'
const findById = vi.fn()
vi.when(findById).calledWith(1).thenResolve({ id: 1, name: 'Ella' })
vi.when(findById).calledWith(2).thenResolve({ id: 2, name: 'Marcus' })
vi.when(findById).calledWith(expect.any(String)).thenReject(new TypeError('ids are numbers'))
Asymmetric matchers work as arguments, so the third line catches a whole class of calls rather than one value. The companion assertion expect(findById).toHaveBeenExhausted() fails the test when a stubbed behavior was never consumed, which catches the quiet rot where a test keeps stubbing a call path the component stopped making months ago.
Is this jest-when absorbed into core? Essentially, yes. And that's the right call: the community proved the API shape over six years, and first-party support means it tracks the mock internals instead of trailing them. When I rewired a React data-layer mock with it while testing for this article, the diff was almost entirely deletions. If your components lean on typed query hooks, the pattern in our React data fetching guide pairs naturally with calledWith on query keys.
Silent failure is a theme this release keeps attacking, from unawaited assertions to unconsumed stubs. If you like that direction, aim the same suspicion at your linter: our free lint coverage audit script runs seven checks for files ESLint never opens and CI steps that can't fail, which is the same disease in a different organ.
Fake Timers Finally Understand Temporal
vi.useFakeTimers() and vi.setSystemTime() now mock the Temporal API alongside Date, courtesy of the @sinonjs/fake-timers 15.4 update. If part of your code reads Temporal.Now while older modules still call Date.now(), both now agree under fake timers, and toNotFake: ['Temporal'] opts back out where you need the real clock.
vi.useFakeTimers({ now: new Date('2026-09-14T09:00:00Z') })
expect(Temporal.Now.instant().toString()).toBe('2026-09-14T09:00:00Z')
Why does that matter now? Because codebases are mid-migration. Nobody flips date handling in one commit, and until this release your tests could not fake time consistently across the seam. This closes it.
Browser Mode Gets a Flight Recorder
How many times have you rerun a flaky browser test purely to watch what the DOM does? Trace View is the feature I'd upgrade for. Enable browser.traceView and every interaction and assertion in a browser test records a DOM snapshot you can step through after a failure, the same investigation style Playwright users have had for years, now inside component tests. A flaky click sequence stops being a matter of rereading the test and imagining the DOM.
Locator failures got smarter too. When getByRole finds nothing, Vitest now prints the ARIA snapshot of the subtree it searched, so the error shows you the roles and names that actually exist instead of leaving you to console.log the DOM. The format is tunable through browser.locators.errorFormat. Combined with strict-by-default locators, browser tests fail with evidence now. That changes review culture more than any single API: a failing screenshot plus an ARIA tree is an argument, not a shrug. We lean on exactly these signals when testing AI-generated React components, where the test author didn't write the markup and needs the runner to describe it honestly.
Vitest 5's real theme is evidence: locator errors print the ARIA tree that actually exists, Trace View replays the DOM at every step, and toHaveBeenExhausted proves your stubs were consumed, so a failing test now argues its case instead of shrugging.
Benchmarks Became Replayable, and That's the Point
The old top-level bench import is gone. Benchmarking is now a test-context fixture inside regular test() calls in benchmark files, with two additions that change what benchmarks are for. bench.compare() runs variants against each other and stores results. bench.from() replays a stored result as a baseline, so CI can answer "did this PR make it slower than main" instead of printing numbers nobody compares.
test('serialize row', async ({ bench }) => {
const stored = await bench.from('./baselines/serialize.json')
await bench.compare(stored, () => serializeRow(fixture))
})
Honestly, most application teams shouldn't adopt this yet. A benchmark that runs on a shared CI runner measures the runner's mood as much as your code, and a baseline file invites false confidence. Library authors with hot paths are the audience here, and for them the platform question in our Node test runner comparison tilts a little further toward Vitest, because node:test offers nothing comparable.
An Adoption Order That Makes Sense
Upgrade for the free speed and let vitest doctor argue about your pool settings. Move your worst mockImplementation switches to vi.when the next time a test in that file fails, not in a big-bang refactor. Turn on browser.traceView the same day if you run Browser Mode. Leave the benchmark API until you have a hot path with a named owner. The deeper story is that the runner now treats silent success as a bug in itself, and having spent a week with it, I think that's the most durable change in the release. Speed estimates age; a test that can prove what it saw doesn't.