A notice on the Vitest model: This migration was carried out in August 2026, utilizing Vitest 4. All of the figures, benchmarks, configuration particulars, and classes described on this article discuss with Vitest 4, not Vitest 5.
Migrating a check runner isn’t a magic “swap and go” operation.
Our expertise shifting our whole frontend check suite from Jest to Vitest was an actual engineering enterprise, crammed with bottlenecks, meticulous pool tuning, unmasked bugs, one rollback, and one optimization we shipped after which needed to take again.
Over the course of about three weeks, our frontend staff migrated eight repositories. We did not accept file counts; we ran each suite end-to-end to get the actual numbers.
Right here is the precise scale of what we achieved:
- 10,144 executed unit checks, actual numbers from truly operating the suites, not file counts.
- 1,799 check recordsdata throughout 8 repositories.
- 0 repos left in a half-migrated state.
- ~3 weeks to finish the complete effort, migrations plus the tuning that adopted.
- ~$751 estimated AI token price for the Claude Code classes we might hint to this work. See the caveats on that quantity beneath: it is a tough higher sure, not a invoice.
That is the story of what truly occurred, drawn straight from our git historical past, PR our bodies, and session logs, that will help you navigate your individual migration.
To avoid wasting you among the ache we went via, we additionally printed a generalized model of the AI automation ability we ended up wishing we might had from day one: github.com/Subito-it/articles-code/tree/fundamental/jest-to-vitest-migration.
Why Jest began hurting
We did not resolve emigrate simply because “Jest is sluggish” within the summary.
We migrated as a result of sustaining it had develop into an lively, recurring tax on our engineering time.
We additionally wished to be on the aspect of the place the ecosystem was heading, since Vitest was quick changing into the default alternative for brand spanking new Vite/Subsequent.js initiatives.
1. The CommonJS vs. ESM nightmare
By default, subsequent/jest skips remodeling something inside node_modules, so when a dependency ships an ESM-only replace, Jest’s CommonJS require() fails.
We discovered ourselves hand-patching transformIgnorePatterns regexes to allow-list particular packages. This is the precise code remark from our config, verbatim:
// ESM-only node_modules that Jest should rework. subsequent/jest overwrites any
// user-supplied transformIgnorePatterns to skip all of node_modules, so we
// allow-list these on the resolved config as a substitute.
const esmPackagesToTransform = ['cookie'];
Individually, and about six weeks earlier than we might even determined emigrate: a significant model bump of uuid shipped as pure ESM and broke Jest outright.
The actual commit message: “uuid v14 ships as pure ESM which breaks Jest (CommonJS). Exchange the one utilization of uuid() … with the native crypto.randomUUID() … eradicating the dependency fully.”
Reasonably than battle the rework config once more, the staff simply deleted the dependency. When your check runner is dictating your dependency decisions earlier than you’ve got even deliberate a migration, that is an actual, quiet price, not a hypothetical one.
2. Ecosystem bloat
Each repo was carrying a fragile pile of Jest-specific tooling: jest, jest-mock, jest-environment-jsdom, ts-jest, @sorts/jest, babel-jest, and repo-specific extras.
Shifting to Vitest’s Vite-based rework pipeline meant shrinking that footprint in every single place.
The good efficiency fable (and the way we mounted it)
Vitest was not robotically sooner than Jest, and we’re not going to faux in any other case.
In at the least two of our repositories, out-of-the-box Vitest was measurably slower till we did actual tuning work.
The employee cap actuality
In a single repo, conserving the identical CI employee cap that labored superb with Jest, turned a ~35-second check run right into a ~3-minute crawl with Vitest. The precise check execution time was near-identical.
The distinction was virtually fully pool/employee overhead.
We mounted it by explicitly elevating maxWorkers from 2 to eight on a many-core CI runner.
The benchmarking sweep
You can not guess your Vitest configuration; you must measure it. One other repo, unrelated to the one above and with its personal separate Jest baseline of 44.4 seconds on CI, ran a rigorous native benchmark sweep on Vitest’s forks pool, on a 10-core machine, for its 146 recordsdata / 683 checks:
- 2 employees: 60.9 seconds
- 4 employees: 35.0 seconds
- 6 employees: 26.6 seconds (the candy spot)
- 8 employees: 27.0 seconds
- 10 employees: 28.5 seconds
Throwing extra employees on the downside did not assist previous 6.
The “sooner” Vitest wasn’t truly sooner than the earlier Jest CI baseline till it was tuned to match the {hardware}.
Pool choice: the setting that really issues
Vitest provides totally different worker-isolation swimming pools, and choosing the proper one is a correctness choice, not only a pace one.
We finally moved each repo to the forks pool.
forks makes use of actual baby processes, so atmosphere variables apply accurately earlier than Node initializes.
That issues as a result of the choice, threads, mutates course of.env too late for issues like TZ to be picked up, leading to timezone-dependent damaged snapshots on CI in certainly one of our repos.
The price of forks is that it re-creates jsdom per file, making it the slower possibility out of the field (one repo measured this at roughly 5x slower than Jest earlier than employee tuning).
We selected forks anyway, as a result of it was essentially the most appropriate possibility: it prevented race situations {that a} shared-context pool (vmThreads) was silently masking, and it prevented an automocking hole that broke error-tracking calls underneath a special pool (subsequent part).
The automocking lure: Jest bugs hidden for years
Jest’s __mocks__/ listing is magical: drop a file in there matching a bundle identify, and Jest silently applies it in every single place within the import graph, together with packages a check file by no means imports straight. Vitest requires an express vi.mock() name within the check file itself.
When that silent Jest magic went away, our checks broke in fascinating methods.
A shared inside telemetry library had a background sync timer that Jest’s implicit mocking had apparently been suppressing for years.
Below Vitest, the actual module ran, the timer fired throughout checks, and we bought unhandled promise rejections, throughout three separate repositories, every found independently relatively than solved as soon as and shared.
Individually, two repos discovered that with one explicit pool configuration, our error-tracking SDK’s calls resolved to undefined as a substitute of operating. A check situation that ought to have triggered an error, did nothing for years, silently.
Each have been mounted the identical method, with an express mock manufacturing unit as a substitute of counting on automocking.
The riskiest case was quieter than both of these: a number of element checks had been counting on Jest silently intercepting a shared inside bundle’s actual implementation.
Below Vitest, with out an express mock, the actual implementation ran as a substitute, and the checks did not fail loudly.
They simply began exercising totally different code paths than those they have been written to confirm, a false-positive-test danger that had been sitting there the entire time Jest was protecting for it.
Normalizing rollbacks
If you happen to’re picturing a tidy “one PR, performed” migration for each repo: neglect it.
One in every of our repos merged a migration PR, hit some non-deterministic flaky checks virtually instantly, and made the best, low-drama name: roll it again with a revert commit, examine correctly, and land a profitable retry about 5 days later.
That is not a manufacturing incident and it is not a failure. It is what a change of this measurement usually appears to be like like when you do not rush the “performed” name.
The sample throughout this complete effort is to measure earlier than declaring victory, and never be afraid to undo one thing that is not working.
The place Jest was truly higher
We do not need this to learn as a pitch, so here is the opposite aspect. Jest, for years, simply labored.
No person on the staff had to consider pool fashions or worker-count sweeps to get a steady, moderately quick suite.
Its __mocks__/ auto-mocking, for all of the false-positive danger it turned out to be hiding, additionally meant no person needed to write an express vi.mock() for each transitively-mocked dependency.
And in at the least two of our repos, out-of-the-box Jest was merely sooner than out-of-the-box Vitest, full cease, till we did tuning work Jest had by no means demanded of us.
In case your suite is small, steady, and never combating ESM dependencies, “migrate to Vitest” isn’t an computerized win.
It is a commerce of 1 set of upkeep prices for an additional, and ours solely got here out forward as a result of our particular ache factors (ESM friction, jsdom-adjacent tooling, hidden automocking bugs) occurred to be those Vitest’s mannequin addresses properly.
The fact: this was not possible with out AI, on this timeline
With out AI help, we would not have accomplished this migration on this timeline.
Manually changing hundreds of check recordsdata throughout eight independently-owned codebases in about three weeks was not life like for a staff our measurement.
We delegated the mechanical conversion (hundreds of repetitive, well-defined transformations) to Claude Code.
What we are able to say in regards to the AI price, exactly:
- Enter tokens: ~250,000 (actual, summed from session logs)
- Output tokens: ~9.7 million (actual)
- Cache learn/write tokens: ~1.46 billion / ~60 million (actual)
- Classes matched: 70, throughout 7 of our 8 repos (our shared element monorepo’s classes weren’t individually mined)
- Estimated price: ~$751 whole, ~$107/repo common
Two trustworthy caveats on that price determine: it is derived from actual token counts utilizing checklist pricing plus an assumed cache-discount multiplier, not an precise invoice.
We do not have entry to actual invoiced numbers for these classes. And our session-relevance filter matched any point out of “migration,” which is broader than “Jest-to-Vitest migration particularly,” so $751 is best learn as an higher sure than a exact price of this one effort.
We have additionally enforced a easy however highly effective idea. These days It is extra vital then ever having a deep data on what we delegate to AI. Earlier than the motion, we spent a while to research Jest-Vitest variations, learn Vitest docs and perceive what plan finest fitted our codebase. With out this step, we might in all probability spent far more tokens.
What Claude Code purchased us wasn’t simply pace on the mechanical work. It let our engineers spend their time on the precise judgment calls: which pool to select, whether or not a “repair” actually helped, when a migration try was truly able to name performed.
The migration ability, printed
The toughest a part of the migration itself wasn’t the API syntax modifications. It was the entire lack of steering on employee benchmarking and pool choice, which is precisely the place we misplaced essentially the most time.
The ability itself wasn’t written as soon as and left alone. We constructed it, then stored adapting it as every repo hit a brand new pool bug or benchmarking shock, so by the point the final repo migrated it already carried the teachings from each one earlier than it. That ongoing adaptation was its personal troublesome half, separate from the migration work itself.
We’re now making that ability obtainable to everybody: the per-file migration guidelines it grew to incorporate, with something company-specific stripped out; seize it and run it by yourself codebase:
github.com/Subito-it/articles-code/tree/fundamental/jest-to-vitest-migration

