What you'll learn
Quick Answer
Jest is the long-established default with the biggest ecosystem and the most Stack Overflow answers. Vitest is a newer runner that reuses Vite's transform pipeline, so it runs modern ESM and TypeScript with no configuration and starts faster on large suites. The assertion and mocking APIs are close enough that migrating is mostly a find-and-replace. Pick Vitest for a new Vite-based project; keep Jest if you have a large existing suite that works.
The ESM difference, demonstrated
This is the single clearest reason Vitest exists. Take one plain ES module and one test that imports it:
// sum.js
export const sum = (a, b) => a + b;
// sum.test.js
import { sum } from './sum.js';
test('adds', () => { expect(sum(2, 3)).toBe(5); });Run this under Vitest and it passes immediately. Run the same file under Jest with no extra setup and it fails with:
Must use import to load ES Module ...
The file contains ESM syntax (import/export) that could not be
executed as CommonJS. Either configure a transform (babel-jest)
... or use Node v24.9+ where Jest supports require(esm) natively.Jest was built in a CommonJS world. Modern Jest can handle ESM, but it needs Babel or ts-jest configured, or a recent enough Node. Vitest gets ESM and TypeScript for free because Vite already transforms your source the same way your app build does - your test environment and your real build stay in sync by construction.
Watch mode: opposite defaults
The bare commands behave differently, and this trips people up in CI.
Jest: jest with no arguments runs the suite once and exits. You add --watch or --watchAll for a watcher.
Vitest: vitest with no arguments watches when it detects an interactive terminal, and runs once when it detects CI or a non-interactive shell. To force a single run regardless, use vitest run.
The practical rule: in package.json, set your test script to vitest run (or jest) so CI always does a single deterministic pass, and add a separate test:watch script for local development. Relying on Vitest's auto-detection works most of the time, but an explicit run removes the ambiguity.
The APIs are nearly the same
Vitest deliberately mirrors Jest's API. describe, test/it, expect, beforeEach, afterAll, and the common matchers - toBe, toEqual, toThrow, toHaveBeenCalledWith - work the same way.
The main differences are namespacing and imports. Jest injects its globals automatically. Vitest can too (with globals: true in config), but by default you import what you use: import { test, expect, vi } from 'vitest'. Mocking is vi.fn() and vi.mock() instead of jest.fn() and jest.mock(). Timer control is vi.useFakeTimers().
For a straightforward suite, migration is mostly replacing jest. with vi. and adding imports. Where it gets involved is custom Jest transformers, jest.config.js plugins, or snapshot serializers - those need porting, and some Jest-ecosystem packages assume the jest global exists.
Speed and startup
Vitest's headline claim is speed, and the mechanism is real: it shares Vite's transform cache and dependency pre-bundling, and it uses a fast watch loop that only re-runs tests affected by a change. On a large suite the difference in re-run time after editing one file can be significant.
For a cold full run, the gap is smaller and depends heavily on your config - the number of worker processes, whether you use jsdom or happy-dom or no DOM, and how heavy your setup files are. Jest is not slow; it is just that Vitest avoids a separate transform step that Jest has to do.
Do not pick a runner on benchmark numbers alone. The developer-experience wins - no transform config, TypeScript that just works, a nice watch UI - matter more day to day than shaving a few seconds off a cold run.
Ecosystem and the migration gotcha
Jest has years of accumulated answers, integrations, and CI recipes. If you hit an obscure error, someone has probably posted the fix. Vitest's ecosystem is younger but covers the common cases well, and Testing Library, MSW, and most assertion extensions support both.
The gotcha when migrating: a floating assertion inside a .then() that is never returned or awaited will not fail the test in either runner - the test finishes green before the assertion runs. This is not a Jest-vs-Vitest difference, but people discover it during a migration when they are already suspicious of the tooling and blame the wrong thing. Always await or return your promises, and consider the eslint-plugin-vitest or eslint-plugin-jest rules that catch it.
Another real one: mock state does not reset between tests unless you configure it (clearMocks/restoreMocks in Jest config, clearMocks in Vitest config, or explicit beforeEach calls). A test that passes alone and fails in the suite is usually leaked mock state.
The verdict
New project, especially with Vite: use Vitest. Zero transform config, TypeScript and ESM work immediately, and your test setup matches your build.
Existing large Jest suite that works: keep it. The migration effort rarely pays for itself unless you are already fighting Jest's ESM story or moving the app to Vite anyway.
Non-Vite project, greenfield: either is fine. Vitest still works without Vite as your bundler; Jest is the safer bet if your team already knows it well.
Whichever you choose, put vitest run or jest in your CI script explicitly, turn on mock resetting, and lint for floating promises. Those three settings prevent most of the flaky-test pain that people wrongly attribute to the runner.
