Vitest vs Jest: ESM, TypeScript and Migration Cost
Speed gets the headlines, but teams move from Jest to Vitest for ESM and TypeScript configuration. Jest ESM support is still documented as experimental, requires a Node flag, and breaks jest.mock().
Jest is at version 30.4 and remains the most widely deployed JavaScript test framework. Vitest is at version 4 and was built on Vite from the start. The comparison is usually framed around speed, which is real but is rarely the reason anyone actually migrates.
The reason is ESM. Jest documentation states plainly that it ships with experimental support for ECMAScript Modules, that the implementation may have bugs and lack features, and that you must run Node with --experimental-vm-modules to enable it. Under ESM, jest.mock() does not work, because static imports are evaluated before your code runs and the hoisting Jest relies on cannot happen. You use jest.unstable_mockModule() instead, and the name is not accidental.
For a codebase that has moved to ESM, or is on TypeScript with a Vite build, that is the whole story. Vitest is ESM-native and reads your existing Vite config. This page covers what the migration costs, what breaks, and the cases where staying on Jest is the right call.
Vitest vs Jest at a glance
| Vitest | Jest | |
|---|---|---|
| Current major | v4 | v30 |
| ESM | Native | Experimental, needs --experimental-vm-modules |
| Module mocking under ESM | vi.mock() | jest.unstable_mockModule() |
| TypeScript | Out of the box via Vite | babel-jest (no type-check) or ts-jest (slower) |
| Config | Reuses vite.config.ts | Separate Jest config and transform chain |
| Assertions | Chai with Jest-compatible expect | expect |
| Watch mode | HMR-style rerun | Standard watch |
| Coverage | v8 or Istanbul | babel-plugin-istanbul or v8 |
| Browser mode | Yes | No (jsdom/happy-dom simulation only) |
| Benchmarking / type testing | Tinybench, expect-type | No |
| Sharding | Yes | Yes, --shard since v28 |
The ESM problem, concretely
This is the difference that turns into hours of work rather than a config line. Enabling ESM in Jest is not one flag, it is a set of interacting requirements, and it changes how you write mocks.
- Static
importstatements are evaluated before your code runs, so the hoistingjest.mock()depends on cannot happen under ESM. require()of an ESM file with top-level await throwsERR_REQUIRE_ASYNC_MODULE.- The
jestobject has to come from@jest/globalsorimport.meta.jestrather than being ambient. - Jest documentation notes the underlying Node APIs it uses for this are themselves experimental.
# Jest, ESM mode
NODE_OPTIONS="$NODE_OPTIONS --experimental-vm-modules" npx jest
# ...plus transform: {} in config (or a transformer emitting ESM),
# ...plus extensionsToTreatAsEsm for .ts/.jsx,
# ...and jest.mock() no longer works:
const { charge } = await import('./payments.js');
jest.unstable_mockModule('./payments.js', () => ({ charge: jest.fn() }));
# Vitest, no flags, no mode
vi.mock('./payments.js', () => ({ charge: vi.fn() }));TypeScript: two bad options versus none needed
Jest offers two TypeScript routes and each has a real cost. Vitest inherits TypeScript from Vite, so there is no third config to maintain.
| Approach | Type-checks tests? | Cost |
|---|---|---|
| Jest + babel-jest | No | Fast, but type errors in tests go unnoticed |
| Jest + ts-jest | Yes | Noticeably slower; type-checks on every run |
| Vitest | Via your existing tsconfig | No extra transform config |
Why the migration is cheaper than it looks
Vitest deliberately implements a Jest-compatible expect and Jest-compatible mocking utilities. That makes the bulk of a migration mechanical rather than a rewrite.
describe,it,test,beforeEachand friends are the same.expect(...)matchers are Jest-compatible, so assertions largely do not change.jest.fn()becomesvi.fn(),jest.spyOn()becomesvi.spyOn(),jest.mock()becomesvi.mock(). Mostly find and replace.- Snapshots are Jest-compatible in format, so existing snapshot files generally carry over.
globals: truein the Vitest config keepsdescribe/it/expectambient, so you do not have to add imports to every test file on day one.
Speed, honestly
Vitest is generally faster, particularly in watch mode where it reruns only what changed through a Vite-style dependency graph. In CI on a cold machine the difference is smaller than benchmarks suggest, because both spend a large share of wall-clock on install and transform rather than on assertions.
- Watch mode is where the gap is most obvious and where developers feel it every day.
- Both support sharding across CI machines, so at large scale the two converge on whatever your parallelism allows.
- If you are on
ts-jest, some of your measured Jest slowness is type-checking on every run, not the runner. Compare againstbabel-jestbefore attributing it to Jest itself.
When to stay on Jest
- Your codebase is CommonJS and staying that way. The main argument for moving does not apply.
- You depend on a custom Jest transformer or a Jest-specific plugin with no Vitest equivalent.
- You are on React Native, where the Jest preset is the well-trodden path.
- Your suite is large, stable, and nobody is complaining. Migration is a real cost against a benefit measured in developer-experience improvements.
Running a migration safely
- Inventory custom transformers, Jest plugins, and anything touching Jest internals. This is the only unbounded part.
- Add Vitest alongside Jest and configure
globals: trueso existing test files need no edits. - Convert one directory. Run both suites in CI and diff the results, not just the pass/fail count.
- Find and replace
jest.withvi.across the converted files, then fix the handful that do not map cleanly. - Regenerate snapshots deliberately and review the diff rather than accepting it wholesale.
- Remove Jest only once the diff is empty and the whole suite has run green on the new runner for a full sprint.
The switching cost is mostly in the parts nobody lists
- Assertions and mocks usually port mechanically when the target implements a compatible API; custom transformers and framework plugins do not.
- Snapshot formats differ between runners, so plan to regenerate and review rather than port.
- Run both suites in parallel in CI for a period and diff the results. A migration that changes which tests fail is not a migration, it is a regression you have not found yet.
- Coverage numbers move on a runner change even when the tests do not, because instrumentation differs. Re-baseline any coverage gate deliberately.
The verdict
On ESM, or on TypeScript with a Vite build: migrate to Vitest. Jest ESM support is documented as experimental, needs --experimental-vm-modules, and forces jest.unstable_mockModule() in place of jest.mock(). That is a permanent tax on every test file that mocks anything.
On CommonJS with a working suite: stay on Jest. The headline reason to move does not apply to you, and Jest v30 is actively maintained.
On ts-jest and finding tests slow: try babel-jest before concluding the runner is the problem. Some of what you are measuring is type-checking on every run.
Whichever way you go, the migration is unusually cheap because Vitest is deliberately Jest-API-compatible. The bounded part is find and replace; the unbounded part is custom transformers, so inventory those before you commit to a plan.
Frequently asked questions
Should I switch from Jest to Vitest?
--experimental-vm-modules Node flag, and replaces jest.mock() with jest.unstable_mockModule(). If you are on CommonJS with a working suite, there is no strong reason to move.Does Jest support ESM?
--experimental-vm-modules, set transform: {} or emit ESM from your transformer, and use extensionsToTreatAsEsm for .ts and .jsx.Why does jest.mock() not work with ESM?
import statements before any of your code runs, so the hoisting that jest.mock() relies on cannot happen. Jest provides jest.unstable_mockModule() for ESM instead, used with a dynamic import() of the module under test.How hard is migrating from Jest to Vitest?
expect and Jest-compatible mocking. jest.fn() becomes vi.fn() and so on, snapshots are format-compatible, and globals: true avoids editing every file. The unbounded part is custom Jest transformers and plugins, so inventory those first.Is Vitest actually faster than Jest?
ts-jest, part of what you attribute to Jest is type-checking on every run.Does Vitest work without Vite?
Can Jest type-check my TypeScript tests?
ts-jest. The babel-jest route transpiles without type-checking, which Jest documentation states explicitly, so type errors in tests pass silently. This catches out teams who assume their test suite is type-checked when it is not.