Skip to content

The New Capabilities in Vitest 5, Ranked by Payoff

Vitest 5.0 ships vi.when, a rewritten bench API, Temporal-aware fake timers, and a browser Trace View. Here is what to adopt first, with numbers.

· · 7 min read
Code editor window filled with JavaScript on a monitor

Quick Take

The migration checklist is one afternoon. The new capabilities are the reason to spend it: per-argument mocking, replayable benchmarks, a Trace View for browser tests, and speedups the Vitest team measured at 8 to 53 percent.

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.when for per-argument mocks, fsModuleCache for 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:

ScenarioVitest 4Vitest 5Change
deps-heavy project, vm pool1.59s0.74s-53%
enterprise-monolith, isolated7.24s5.83s-19%
React/Vue SPA, Browser Modebaselinefaster-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.

Close-up of a stopwatch face against a black background
Photo by William Warby on Unsplash

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.

Coil of photographic negative film unrolled on a surface
Photo by Eric TERRADE on Unsplash

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.

Share this Post on X Bluesky

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.

Frequently Asked Questions

Is Vitest 5 actually faster, and by how much?
Yes, and the numbers come from the team's own public benchmark suite at vitest-dev/benchmarks rather than a marketing page. VM pools improved the most: a deps-heavy project dropped from 1.59s to 0.74s, about 53 percent. A 1,280-module monolith with isolation on fell 19 percent, from 7.24s to 5.83s, and Browser Mode runs landed 16 to 18 percent faster on React and Vue SPA suites. Across configurations the range is 8 to 25 percent.
What does vi.when add over mockImplementation?
Per-argument behavior on a spy. vi.when(fn).calledWith(1).thenResolve(user) replaces the switch statement inside a mockImplementation callback, accepts asymmetric matchers as arguments, and pairs with the toHaveBeenExhausted assertion to verify every stubbed call was actually consumed. It reads like a spec table instead of control flow.
Do I need Browser Mode to benefit from Vitest 5?
No. The vm pool speedup, fsModuleCache, vi.when, the benchmark rewrite, Temporal fake timers, and the vitest doctor command all apply to plain jsdom or node projects. Browser Mode gets the flashiest additions, Trace View and ARIA snapshots in locator errors, but the release is not gated on it.