Jest has been the default JavaScript testing framework for years. Vitest, built on Vite and released in 2022, is now the fastest-growing alternative — and in most modern projects it's the better choice. The key difference: Jest uses its own module system with Babel transforms; Vitest reuses your existing Vite config and runs natively in ESM. This guide tells you exactly when that matters and which to pick.
At a glance
| Vitest | Jest | |
|---|---|---|
| Vendor | Vitest team (community) | Meta (open-source) |
| Released | 2022 | 2014 |
| Runtime | Vite + esbuild | Babel / ts-jest / @swc/jest |
| Module system | ESM-first, CJS supported | CommonJS-first, ESM experimental |
| Config | Reuses vite.config.ts |
Separate jest.config.{js,ts} |
| Speed (cold) | ⚡ 2–10× faster | Slower (Babel transform overhead) |
| Watch mode | Smart (only re-runs affected) | Watches all test files |
| UI | @vitest/ui browser dashboard |
Limited (third-party) |
| Snapshot testing | ✅ | ✅ |
| Coverage | V8 or Istanbul | Istanbul (via babel-jest) |
| TypeScript | Native (no extra config) | Needs ts-jest or @swc/jest |
| Best for | Vite projects, modern ESM, React/Vue/Svelte | Legacy CJS apps, React (CRA), mature monorepos |
What is Jest?
Jest is a JavaScript testing framework developed by Facebook (Meta). It includes a test runner, assertion library, mocking system, snapshot testing, and coverage tool — all in one package. It works with any JavaScript project and has a massive ecosystem.
npm install --save-dev jest @types/jest
# For TypeScript
npm install --save-dev ts-jest
Key Jest strengths:
- Mature ecosystem with 8+ years of production use
- Works with any bundler (Webpack, Rollup, no bundler)
- Extensive community plugins and integrations
- Native CommonJS support, no config needed
- Built-in fake timers, module mocking, and spy functions
What is Vitest?
Vitest is a testing framework built on top of Vite. It reuses your Vite configuration (plugins, aliases, transforms) so your tests run in the same environment as your production code. No extra Babel pipeline, no duplicate config.
npm install --save-dev vitest
# Uses your existing vite.config.ts — no additional config needed
Key Vitest strengths:
- Reuses Vite config (path aliases, plugins, transforms work out of the box)
- Native ESM — no "Cannot use import statement" errors
- esbuild for transforms = dramatically faster cold start
- Smart watch mode re-runs only files affected by your changes
@vitest/ui— browser-based test dashboard with coverage maps- Jest-compatible API — most test code migrates without changes
Core API comparison
Both frameworks use the same test API names, so migration is mostly config-level:
// ✅ This code works identically in both Jest and Vitest
import { describe, it, expect, vi, beforeEach } from 'vitest'
// vs Jest: import { describe, it, expect, jest, beforeEach } from '@jest/globals'
describe('UserService', () => {
let service: UserService
beforeEach(() => {
service = new UserService()
})
it('returns user by id', async () => {
const user = await service.findById(1)
expect(user).toEqual({ id: 1, name: 'Alice' })
})
it('throws if not found', async () => {
await expect(service.findById(999)).rejects.toThrow('User not found')
})
})
The main API difference: Vitest uses vi instead of jest for mocking utilities.
// Jest
jest.mock('../api')
jest.spyOn(console, 'error').mockImplementation(() => {})
jest.useFakeTimers()
// Vitest
vi.mock('../api')
vi.spyOn(console, 'error').mockImplementation(() => {})
vi.useFakeTimers()
Performance comparison
| Scenario | Vitest | Jest (Babel) | Jest (SWC) |
|---|---|---|---|
| Cold start (small suite) | ~0.3s | ~2.5s | ~1.0s |
| Cold start (large suite 500 tests) | ~2s | ~15s | ~6s |
| Watch mode re-run (1 file changed) | ~0.1s | ~1.5s | ~0.6s |
| Parallel test files | ✅ Worker threads | ✅ Child processes | ✅ Child processes |
| HMR in watch mode | ✅ Vite HMR | ❌ Full re-transform | ❌ Full re-transform |
| Memory usage (large suite) | Lower (shared Vite pipeline) | Higher (per-worker Babel) | Medium |
Bottom line: Vitest is 2–10× faster in real-world projects, primarily because esbuild transforms are near-instant vs Babel. The gap is largest on cold starts and watch mode.
ESM support
This is where Jest has historically caused the most pain:
// Jest — you need this to handle ESM (package.json)
{
"jest": {
"transform": {},
"extensionsToTreatAsEsm": [".ts"]
}
}
# Jest — you often need this flag (still "experimental" as of 2025)
NODE_OPTIONS=--experimental-vm-modules npx jest
// Vitest — ESM just works
// vite.config.ts already handles it, no flags needed
Common Jest ESM errors Vitest eliminates:
SyntaxError: Cannot use import statement outside a moduleSyntaxError: Unexpected token 'export'Must use import to load ES Modulefrom npm packagesCannot find modulewhen packages ship ESM-only builds
With the JavaScript ecosystem moving heavily to ESM (Lodash-es, Chalk 5+, uuid, nanoid, etc.), this is a significant practical advantage for Vitest.
TypeScript support
| Vitest | Jest | |
|---|---|---|
| TypeScript out of the box | ✅ via esbuild | ❌ needs ts-jest or @swc/jest |
| Type-checking in tests | tsc --noEmit separately |
ts-jest can type-check (slower) |
tsconfig.json path aliases |
✅ auto from vite.config | ❌ needs moduleNameMapper |
| Decorators (NestJS) | Needs @swc-node/register or Babel |
Needs ts-jest + babel-jest |
vitest/globals types |
Auto with globals: true config |
Needs @types/jest |
Vitest TypeScript setup (zero config):
// vite.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true, // so you don't need to import describe/it/expect
environment: 'jsdom' // for React component tests
}
})
Jest TypeScript setup:
// jest.config.ts
export default {
preset: 'ts-jest',
testEnvironment: 'jsdom',
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1' // must duplicate tsconfig paths here
}
}
Mocking comparison
Both have equivalent mocking capabilities:
// Automatic module mock — Vitest
vi.mock('./db', () => ({
query: vi.fn().mockResolvedValue([{ id: 1 }])
}))
// Automatic module mock — Jest
jest.mock('./db', () => ({
query: jest.fn().mockResolvedValue([{ id: 1 }])
}))
// Spy on a method — Vitest
const spy = vi.spyOn(api, 'fetchUser').mockResolvedValue({ id: 1, name: 'Bob' })
expect(spy).toHaveBeenCalledWith(1)
spy.mockRestore()
// Spy on a method — Jest (identical except 'jest' → 'vi')
const spy = jest.spyOn(api, 'fetchUser').mockResolvedValue({ id: 1, name: 'Bob' })
// Fake timers — Vitest
vi.useFakeTimers()
vi.advanceTimersByTime(5000)
vi.useRealTimers()
// Date mocking — Vitest
vi.setSystemTime(new Date('2025-01-01'))
Vitest-only feature: vi.importMock
// Mock ESM modules without hoisting issues
const { fetchData } = await vi.importMock('./api')
Snapshot testing
// Both frameworks — identical syntax
it('renders correctly', () => {
const tree = render(<Button>Click me</Button>)
expect(tree).toMatchSnapshot()
})
// Inline snapshots — identical in both
expect(result).toMatchInlineSnapshot(`
{
"id": 1,
"name": "Alice",
}
`)
Vitest adds toMatchFileSnapshot() for snapshotting to arbitrary files:
await expect(html).toMatchFileSnapshot('./snapshots/button.html')
Coverage
| Vitest | Jest | |
|---|---|---|
| Providers | V8 (native), Istanbul | Istanbul (via babel-jest) |
| V8 coverage | ✅ (faster, no instrumentation) | ❌ needs c8 separately |
| Config | coverage.provider: 'v8' |
--coverage flag |
| UI integration | ✅ Coverage map in @vitest/ui |
❌ separate HTML report |
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
exclude: ['node_modules', 'dist']
}
}
})
# Run with coverage
npx vitest run --coverage
npx jest --coverage
Testing environments
| Environment | Vitest | Jest |
|---|---|---|
node (default) |
✅ | ✅ |
jsdom (browser-like DOM) |
✅ @vitest/environment-jsdom |
✅ default option |
happy-dom (faster DOM) |
✅ @vitest/environment-happy-dom |
❌ |
edge-runtime (Cloudflare/Vercel) |
✅ @vitest/environment-edge-runtime |
❌ |
| Per-file environment | ✅ @vitest-environment jsdom comment |
✅ @jest-environment jsdom comment |
// Vitest — set environment per file
// @vitest-environment jsdom
import { render, screen } from '@testing-library/react'
import { Button } from './Button'
test('renders button', () => {
render(<Button>Click</Button>)
expect(screen.getByText('Click')).toBeInTheDocument()
})
@vitest/ui — Visual test dashboard
Vitest includes a browser UI with no extra setup:
npx vitest --ui
# Opens http://localhost:51204/__vitest__/
The UI shows:
- All test suites with pass/fail status
- Test duration per suite and per test
- Coverage map (if enabled)
- File dependency graph
- Console output per test
Jest has no built-in UI. Third-party options (jest-html-reporter, majestic) require separate installation.
Where Vitest wins
| Scenario | Why Vitest is better |
|---|---|
| Vite-based projects (React/Vue/Svelte) | Zero config — reuses your Vite setup |
| ESM-only packages | No NODE_OPTIONS flags or experimental mode |
| TypeScript (no config) | esbuild handles TS natively, path aliases auto-resolved |
| Fast feedback loop | 2–10× faster cold starts, smart watch mode |
| Modern monorepos | Works with pnpm/turborepo/Nx Vite pipelines |
| Coverage reports | V8 native coverage — no instrumentation overhead |
| Visual test runner | @vitest/ui browser dashboard built-in |
| Edge runtime testing | @vitest/environment-edge-runtime for Cloudflare Workers |
| New projects | Less config, fewer dependencies, better defaults |
Where Jest wins
| Scenario | Why Jest is better |
|---|---|
| Webpack/CRA projects | No Vite dependency needed |
| CommonJS-only codebases | Battle-tested CJS support with no config |
| NestJS (non-Vite) | @nestjs/testing + ts-jest is well-documented |
| Large team standardisation | More developers know Jest; more SO answers |
| Extensive plugins | 8+ years of ecosystem (jest-axe, jest-localstorage-mock, etc.) |
| Angular (non-Vite) | Angular TestBed integrates with Jest via jest-preset-angular |
| Mature monorepos | If existing Jest config is complex and working, don't change it |
React Testing Library — works with both
React Testing Library (RTL) is framework-agnostic. The test code is identical:
// Works with both Jest and Vitest
import { render, screen, fireEvent } from '@testing-library/react'
import { Counter } from './Counter'
test('increments count', async () => {
render(<Counter />)
expect(screen.getByText('Count: 0')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: /increment/i }))
expect(screen.getByText('Count: 1')).toBeInTheDocument()
})
Setup difference:
// Jest (jest.config.ts)
setupFilesAfterFramework: ['@testing-library/jest-dom']
// Vitest (vitest.config.ts or vite.config.ts)
test: {
setupFiles: ['@testing-library/jest-dom/vitest'],
// or simply: ['@testing-library/jest-dom'] — recent versions auto-detect
}
Migrating from Jest to Vitest
Most Jest projects can migrate in under an hour:
Step 1 — Install Vitest
npm uninstall jest @types/jest ts-jest babel-jest jest-environment-jsdom
npm install --save-dev vitest @vitest/coverage-v8
# For React/DOM:
npm install --save-dev jsdom @testing-library/jest-dom
Step 2 — Replace jest.config.ts
// Before: jest.config.ts
export default {
preset: 'ts-jest',
testEnvironment: 'jsdom',
moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1' },
setupFilesAfterFramework: ['<rootDir>/jest.setup.ts']
}
// After: vitest.config.ts (or add `test:` block to vite.config.ts)
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
// path aliases auto-resolved from vite.config.ts if present
}
})
Step 3 — Replace jest imports with vi
# Find all jest.* usages
grep -r "jest\." src --include="*.test.ts" --include="*.spec.ts"
# Replace
sed -i 's/jest\./vi\./g' **/*.test.ts
sed -i "s/import { jest }/import { vi }/" **/*.test.ts
Step 4 — Update package.json scripts
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage"
}
}
Step 5 — Fix common migration issues
| Issue | Fix |
|---|---|
jest.fn() → use vi.fn() |
Global find-replace |
jest.mock() hoisting |
Vitest hoists vi.mock() too — usually works |
__mocks__ folder |
Vitest respects __mocks__ directories |
jest.config env vars |
Move to vite.config.ts define: {} |
moduleNameMapper |
Delete — path aliases from tsconfig.json auto-work |
testPathIgnorePatterns |
Use exclude in Vitest config |
testMatch |
Use include in Vitest config |
clearMocks/restoreMocks |
Same options in Vitest config |
Full comparison table
| Feature | Vitest | Jest |
|---|---|---|
| Created | 2022 | 2014 |
| Bundler integration | Vite (esbuild) | None (Babel/SWC/ts-jest) |
| ESM native | ✅ | ⚠️ Experimental |
| TypeScript | ✅ Built-in | ❌ Needs ts-jest/SWC |
| Speed | ⚡ Very fast | Slower |
| Watch mode | Smart (Vite HMR) | Full rescan |
| Config | Reuses vite.config | Separate config |
| Path aliases | ✅ Auto | ❌ Manual moduleNameMapper |
| Globals (describe/it) | Optional | Always available |
| Snapshot testing | ✅ | ✅ |
| Inline snapshots | ✅ | ✅ |
| File snapshots | ✅ | ❌ |
| Mocking API | vi.* |
jest.* |
| ESM module mocks | ✅ vi.importMock |
❌ Hoisting issues |
| Fake timers | ✅ vi.useFakeTimers |
✅ jest.useFakeTimers |
| Coverage | V8 or Istanbul | Istanbul |
| Visual UI | ✅ @vitest/ui |
❌ (third-party) |
| jsdom | ✅ | ✅ (default option) |
| happy-dom | ✅ | ❌ |
| Edge runtime | ✅ | ❌ |
| CJS projects | ✅ (supported) | ✅ (native) |
| Webpack projects | ⚠️ Needs separate config | ✅ |
| NestJS | ⚠️ Possible but less common | ✅ Well-documented |
| Angular | ⚠️ With @analogjs/vitest |
✅ jest-preset-angular |
| Ecosystem maturity | Growing fast | Very mature |
| npm weekly downloads | ~10M | ~30M |
| Stars (GitHub) | 13k+ | 44k+ |
Decision guide
Choose Vitest if:
- Your project uses Vite (React + Vite, Vue, Svelte, SvelteKit, Nuxt 3, Astro)
- You use ESM packages (uuid, nanoid, chalk v5+, etc.)
- You want TypeScript to work without extra configuration
- You want faster feedback in watch mode
- You're starting a new project
Choose Jest if:
- Your project uses Webpack (Create React App, older setups)
- You have an existing complex Jest configuration that works
- You use NestJS (excellent Jest integration via
@nestjs/testing) - Your team standardises on Jest across multiple projects
- You use Angular (without Vite)
When to migrate:
- New project: use Vitest by default with Vite
- Existing Vite project: migrate when ESM issues appear or you want faster tests
- Non-Vite project: evaluate whether the migration complexity is worth the speed gain
Vitest vs Jest vs Mocha vs Jasmine
| Vitest | Jest | Mocha | Jasmine | |
|---|---|---|---|---|
| All-in-one | ✅ | ✅ | ❌ | ✅ |
| Speed | ⚡ Fastest | Fast | Slower | Slowest |
| ESM | ✅ | ⚠️ | ✅ | ⚠️ |
| TypeScript | ✅ | With plugin | With plugin | With plugin |
| Mocking | Built-in (vi) |
Built-in (jest) |
Needs Sinon | Built-in (limited) |
| Coverage | V8/Istanbul | Istanbul | Needs nyc/c8 | Needs Istanbul |
| Snapshot | ✅ | ✅ | ❌ | ❌ |
| Browser UI | ✅ | ❌ | ❌ | ❌ |
| Framework | Vite | None | None | None |
| Best for | Vite projects | Any JS/TS | Node.js APIs | Angular (legacy) |
Common mistakes
| Mistake | Fix |
|---|---|
Using jest.fn() instead of vi.fn() in Vitest |
Global find-replace: jest. → vi. |
Not setting globals: true in Vitest config |
Add globals: true to avoid importing describe/it/expect in every file |
Not adding @testing-library/jest-dom matchers in Vitest |
Add setupFiles: ['@testing-library/jest-dom/vitest'] to config |
Running vitest in CI without --run flag |
Use vitest run in CI (watch mode not appropriate for CI) |
| Trying Vitest in a Webpack project without Vite | Add a vitest.config.ts with standalone esbuild config |
| Mocking CJS modules in Vitest ESM mode | Use vi.mock() at top of file (hoisted), or vi.importMock() |
Not using v8 coverage (using Istanbul in Vitest) |
provider: 'v8' is faster and more accurate with esbuild |
| Expecting Jest snapshot format in Vitest | Minor format differences — update snapshots with vitest run -u |
FAQ
Is Vitest a drop-in replacement for Jest?
For most projects, yes — especially Vite-based ones. The API is intentionally Jest-compatible. The main change is jest.* → vi.*. Edge cases: __mocks__ behaviour is slightly different, and ESM module mocking works better in Vitest.
Does Vitest work without Vite?
Yes. You can use Vitest in any project by providing a standalone vitest.config.ts. You don't need a Vite dev server. The benefit is esbuild transforms and ESM support, even without the Vite ecosystem.
Should I use @swc/jest for speed instead of migrating to Vitest?@swc/jest (or babel-jest with esbuild) does close the speed gap significantly — cold start drops from ~15s to ~6s. But Vitest at ~2s is still 3× faster, and you get native ESM and zero path-alias config as bonuses.
Is Vitest production-ready?
Yes. As of 2025, Vitest is at v3.x and is used by major projects: Nuxt, SvelteKit, Astro, Vue 3 core, VueUse, Vite itself, and thousands of production apps. It's stable for serious projects.
Can I use Vitest and Jest in the same monorepo?
Yes. Each package can have its own testing setup. Common pattern: new packages use Vitest; legacy packages keep Jest until they're migrated.
What about Jest's fake timer implementation vs Vitest's?
Both use @sinonjs/fake-timers under the hood as of recent versions, so behaviour is nearly identical. Use vi.useFakeTimers() / vi.useRealTimers() in Vitest instead of jest.useFakeTimers().