Skip to content
Latchkey

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

VitestJest
Current majorv4v30
ESMNativeExperimental, needs --experimental-vm-modules
Module mocking under ESMvi.mock()jest.unstable_mockModule()
TypeScriptOut of the box via Vitebabel-jest (no type-check) or ts-jest (slower)
ConfigReuses vite.config.tsSeparate Jest config and transform chain
AssertionsChai with Jest-compatible expectexpect
Watch modeHMR-style rerunStandard watch
Coveragev8 or Istanbulbabel-plugin-istanbul or v8
Browser modeYesNo (jsdom/happy-dom simulation only)
Benchmarking / type testingTinybench, expect-typeNo
ShardingYesYes, --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 import statements are evaluated before your code runs, so the hoisting jest.mock() depends on cannot happen under ESM.
  • require() of an ESM file with top-level await throws ERR_REQUIRE_ASYNC_MODULE.
  • The jest object has to come from @jest/globals or import.meta.jest rather than being ambient.
  • Jest documentation notes the underlying Node APIs it uses for this are themselves experimental.
Mocking a module under ESM
# 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.

ApproachType-checks tests?Cost
Jest + babel-jestNoFast, but type errors in tests go unnoticed
Jest + ts-jestYesNoticeably slower; type-checks on every run
VitestVia your existing tsconfigNo 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, beforeEach and friends are the same.
  • expect(...) matchers are Jest-compatible, so assertions largely do not change.
  • jest.fn() becomes vi.fn(), jest.spyOn() becomes vi.spyOn(), jest.mock() becomes vi.mock(). Mostly find and replace.
  • Snapshots are Jest-compatible in format, so existing snapshot files generally carry over.
  • globals: true in the Vitest config keeps describe/it/expect ambient, 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 against babel-jest before 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

  1. Inventory custom transformers, Jest plugins, and anything touching Jest internals. This is the only unbounded part.
  2. Add Vitest alongside Jest and configure globals: true so existing test files need no edits.
  3. Convert one directory. Run both suites in CI and diff the results, not just the pass/fail count.
  4. Find and replace jest. with vi. across the converted files, then fix the handful that do not map cleanly.
  5. Regenerate snapshots deliberately and review the diff rather than accepting it wholesale.
  6. 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?
If your codebase is ESM, or TypeScript on a Vite build, yes. Jest ESM support is still documented as experimental, requires the --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?
Experimentally. Jest documentation states the implementation may have bugs and lack features, and that the underlying Node APIs it depends on are also experimental. You must run Node with --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?
ESM evaluates static 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?
The bulk is mechanical, because Vitest implements a Jest-compatible 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?
Generally yes, most noticeably in watch mode where it reruns only what changed. In CI on a cold machine the gap narrows, because both spend much of their wall-clock on install and transform. If you are on ts-jest, part of what you attribute to Jest is type-checking on every run.
Does Vitest work without Vite?
Yes. Vitest can run against a project with no Vite build, and it will create the config it needs. The advantage is largest when you already have a Vite config, because then your tests and your app resolve modules identically.
Can Jest type-check my TypeScript tests?
Only with 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.
Is Jest deprecated?
No. Jest is at version 30.4 and actively maintained. Vitest has strong momentum, particularly in the Vite ecosystem, but "everyone is moving" is not a technical reason and a working suite is worth more than a fashionable one.

Related guides

References

Run this faster and cheaper on Latchkey managed runners - self-healing included. Start free → 30-day trial · No credit card