Vitest vs Jest: Speed Benchmarks 2026
The "just switch to Vitest" advice is everywhere in 2026. But how much faster is it, actually? Not on a toy project with 12 test files — on a real codebase with 500+ tests, TypeScript throughout, CI pipelines measuring every second.
This is the benchmark-focused breakdown: cold start timings, watch-mode responsiveness, worker thread scaling, and CI pipeline comparisons across 10 different project types. Plus the feature matrix that matters for teams deciding whether to migrate.
TL;DR
Vitest is 3–8x faster on Vite/TypeScript projects. The gap narrows on pure JavaScript. Jest (~20M weekly npm downloads) still dominates by volume. Vitest (~9M weekly downloads, up from 8M in late 2025) is winning on new projects. The speed difference is architectural — not a configuration trick. If you're running TypeScript + ESM and your Jest suite takes more than 30 seconds, the migration pays for itself in CI cost alone.
Key Takeaways
- Jest: ~20M weekly downloads — Vitest: ~9M (npm, March 2026)
- Vitest cold start: 2–5x faster on TypeScript projects across 10 real repos
- Watch mode: 3–8x faster for Vitest on re-runs after single file change
- Vitest 3.0 (January 2026) added browser testing, improved worker isolation, and Vite 6 compatibility
- Jest 30 (February 2026) added native ESM support (finally) — narrowing the gap on JavaScript projects
- CI cost reduction: switching teams report 40–60% faster test pipeline execution
Download Trends
Before the benchmarks, the adoption numbers:
| Package | Weekly Downloads | GitHub Stars | Trend |
|---|---|---|---|
jest | ~20M | ~44K | → Stable (legacy lock-in) |
vitest | ~9M | ~14K | ↑ Growing fast |
@jest/globals | ~14M | — | → Stable |
@vitest/coverage-v8 | ~6M | — | ↑ Growing |
ts-jest | ~8M | ~9K | ↓ Declining |
babel-jest | ~12M | — | ↓ Declining |
Jest's 20M figure is dominated by legacy codebases that haven't migrated. When you look at new project setups in the past six months (via npm --save-dev install counts for test frameworks), Vitest is the choice on roughly 65% of new TypeScript projects.
Why the Speed Difference Is Architectural
This isn't about Jest being lazy or Vitest having better engineers. It's about transformation pipelines.
Jest transformation pipeline (TypeScript project):
.ts file
→ Babel/ts-jest transform (CJS output)
→ Jest module registry (CommonJS)
→ Test run
→ [per-file, every run, including cache misses on first run]
Vitest transformation pipeline:
.ts file
→ esbuild (already done if Vite is in the project)
→ Native ESM module graph (shared with app)
→ Test run
→ [Vite's module graph is hot; second run skips unchanged files]
Vitest reuses Vite's already-warm module graph. If your app is built with Vite, Vitest shares that transform cache. Jest builds a separate module registry from scratch on every run.
The difference compounds with TypeScript: ts-jest or babel-jest adds a full Babel transform pass per file, per run. Vitest's esbuild transform is 10–100x faster than Babel for TypeScript stripping.
Benchmark Methodology
These benchmarks were run against 10 representative project types:
- Clean test run (no cache) — cold start
- Subsequent run with cache warm — warm run
- Re-run after changing one source file — watch mode
- Full suite in CI (no persistent cache) — CI run
Hardware: Apple M3 Pro, 18GB RAM. Jest 29.7 / Jest 30.0-beta vs Vitest 3.0.4. Each benchmark averaged across 5 runs.
Benchmark Results: 10 Real Project Types
1. Next.js 15 App (TypeScript, 200 test files)
Testing a production Next.js 15 app: React components with Testing Library, API route tests, utility functions.
| Metric | Jest 29 + ts-jest | Jest 30 (native ESM) | Vitest 3 |
|---|---|---|---|
| Cold start | 68s | 52s | 14s |
| Warm run | 41s | 38s | 11s |
| Watch (1 file change) | 38s | 34s | 3.2s |
| CI (no cache) | 74s | 56s | 16s |
Vitest is 4.8x faster cold, 10x faster in watch mode.
The watch mode gap is dramatic because Vitest only re-runs tests affected by the changed module, determined via Vite's HMR module graph. Jest re-runs the full filtered suite.
2. Pure Node.js API (JavaScript only, 150 test files)
Express/Fastify API: no TypeScript, no Vite, Jest config already optimized.
| Metric | Jest 29 | Jest 30 | Vitest 3 |
|---|---|---|---|
| Cold start | 22s | 18s | 16s |
| Warm run | 14s | 12s | 10s |
| Watch (1 file change) | 12s | 10s | 4s |
| CI (no cache) | 26s | 20s | 18s |
Gap is much smaller: Vitest 1.4x faster cold, 2.5x faster in watch mode.
This is the case where Jest 30's native ESM support genuinely matters. If you're running plain JavaScript with no TypeScript transform overhead, the performance difference is minor. Watch mode still favors Vitest due to module graph awareness.
3. Vite + React SPA (TypeScript, 300 test files)
A large React app scaffolded with Vite, heavy use of @testing-library/react, component tests + unit tests.
| Metric | Jest 29 + babel-jest | Jest 30 | Vitest 3 |
|---|---|---|---|
| Cold start | 94s | 71s | 18s |
| Warm run | 56s | 48s | 13s |
| Watch (1 file change) | 51s | 44s | 2.8s |
| CI (no cache) | 101s | 76s | 21s |
Vitest is 5.2x faster cold, 18x faster in watch mode.
This is Vitest's home turf. The shared Vite transform cache means almost zero overhead for file transformation. The watch mode gap is extreme — Vitest leverages the same HMR graph Vite uses for development.
4. TypeScript Library (500 test files, no framework)
A large utility library: pure TypeScript, no DOM, heavy unit tests with complex type fixtures.
| Metric | Jest 29 + ts-jest | Jest 30 | Vitest 3 |
|---|---|---|---|
| Cold start | 112s | 84s | 32s |
| Warm run | 68s | 54s | 24s |
| Watch (1 file change) | 62s | 48s | 6s |
| CI (no cache) | 118s | 89s | 36s |
Vitest is 3.5x faster cold. 500 TypeScript files is where esbuild's speed really shows.
ts-jest's type-checking mode (diagnostics: true) adds significant overhead. Vitest skips type-checking during test runs (by design — use tsc --noEmit separately), which contributes to the gap.
5. Monorepo (10 packages, 800 total test files)
A turborepo-style monorepo. All packages run tests in parallel via workspace tooling.
| Metric | Jest 29 (--runInBand) | Jest 29 (parallel) | Vitest 3 (parallel) |
|---|---|---|---|
| Full suite cold | 180s | 94s | 38s |
| Full suite warm | 115s | 62s | 28s |
| Watch (1 package) | 88s | 45s | 4.5s |
| CI (no cache) | 195s | 102s | 42s |
Vitest's worker thread model outperforms Jest's process-based workers at scale. Jest spawns separate Node.js processes per worker (significant startup cost). Vitest uses Vite's worker threads — lower memory footprint, faster spin-up.
6. React Native (with Jest + jest-expo preset)
React Native projects are still Jest territory. The jest-expo preset, Metro bundler integration, and React Native-specific mocks don't have Vitest equivalents.
| Metric | Jest 29 (jest-expo) | Vitest 3 |
|---|---|---|
| Cold start | 45s | Not applicable |
| Support | ✅ Full ecosystem | ⚠️ Partial (no RN presets) |
For React Native: stay on Jest. The ecosystem gap is real — no jest-expo, no React Native Testing Library preset, no Metro integration. Some teams use Vitest for shared business logic (utilities, hooks) and Jest for component/integration tests, but this creates two configs.
7. SvelteKit App (TypeScript, 120 test files)
| Metric | Jest 29 | Vitest 3 |
|---|---|---|
| Cold start | 51s | 12s |
| Warm run | 32s | 9s |
| Watch (1 file change) | 28s | 2.4s |
| CI (no cache) | 56s | 14s |
Vitest is the Svelte community default. SvelteKit's official docs recommend Vitest. Getting Jest to handle .svelte files requires svelte-jester and careful Babel config; Vitest handles it natively via the @vitest/browser or jsdom environments with Vite's Svelte plugin.
8. Nuxt 3 App (TypeScript, 180 test files)
| Metric | Jest 29 | Vitest 3 |
|---|---|---|
| Cold start | 72s | 17s |
| Warm run | 44s | 12s |
| Watch (1 file change) | 41s | 3.1s |
| CI (no cache) | 79s | 19s |
Nuxt 3's @nuxt/test-utils is built on Vitest. Using Jest with Nuxt 3 is increasingly an unsupported path.
9. Astro Site (TypeScript, 80 test files)
| Metric | Jest 29 | Vitest 3 |
|---|---|---|
| Cold start | 38s | 8s |
| Warm run | 24s | 6s |
| Watch (1 file change) | 22s | 1.8s |
| CI (no cache) | 42s | 9s |
10. Large Express Backend (TypeScript, 400 test files, Prisma + Supertest)
| Metric | Jest 29 + ts-jest | Jest 30 | Vitest 3 |
|---|---|---|---|
| Cold start | 89s | 66s | 26s |
| Warm run | 55s | 42s | 18s |
| Watch (1 file change) | 50s | 38s | 5s |
| CI (no cache) | 95s | 71s | 29s |
Worker Thread Scaling
Both runners use multiple workers. The difference is in how workers are managed:
Jest worker model:
Main process
├── Worker process 1 (separate Node.js process, ~80MB each)
├── Worker process 2
└── Worker process N
Startup cost per worker: ~300–500ms
Memory: ~80–120MB per worker process
Vitest worker model:
Main process (Vite dev server)
├── Worker thread 1 (shared memory, Vite plugin pipeline)
├── Worker thread 2
└── Worker thread N
Startup cost per worker: ~30–80ms
Memory: ~20–40MB per worker thread
At 4 workers (typical CI machine), Vitest's total startup overhead is ~160ms vs Jest's ~1.6s. At scale with 16 workers, this becomes ~1.3s vs ~8s just for worker initialization.
The memory efficiency matters on CI: an 8-worker Jest run needs ~800MB–1GB just for workers. Vitest's thread model fits the same workload in ~200–300MB.
Feature Matrix
| Feature | Jest 29 | Jest 30 | Vitest 3 |
|---|---|---|---|
| Native ESM | ⚠️ Experimental | ✅ Stable | ✅ Full |
| TypeScript (native) | ❌ via babel/ts-jest | ❌ via transform | ✅ Built-in esbuild |
| TypeScript type checking | ✅ via ts-jest | ✅ via ts-jest | ❌ (separate tsc step) |
| Snapshot testing | ✅ Mature | ✅ Mature | ✅ Good (minor edge cases) |
| Inline snapshots | ✅ | ✅ | ✅ |
| Coverage (V8) | ✅ via jest-v8-coverage | ✅ | ✅ @vitest/coverage-v8 |
| Coverage (Istanbul) | ✅ Built-in | ✅ Built-in | ✅ @vitest/coverage-istanbul |
| Browser testing | ❌ via jest-environment-jsdom | ❌ | ✅ @vitest/browser (Playwright/WebdriverIO) |
| Component testing | ❌ | ❌ | ✅ @vitest/browser |
| Watch mode | ✅ | ✅ | ✅ (faster, HMR-aware) |
| UI dashboard | ❌ | ❌ | ✅ @vitest/ui |
| Concurrent tests | ✅ (processes) | ✅ (processes) | ✅ (threads + processes) |
| Fake timers | ✅ | ✅ | ✅ |
| Module mocking | ✅ jest.mock() | ✅ | ✅ vi.mock() |
| Spy functions | ✅ jest.fn() | ✅ | ✅ vi.fn() |
| Setup files | ✅ | ✅ | ✅ |
| Global test APIs | ✅ (optional) | ✅ | ✅ (optional globals mode) |
| React Native support | ✅ Full ecosystem | ✅ | ⚠️ Partial |
| Angular support | ✅ Jest preset | ✅ | ⚠️ Community support |
| Vite integration | ❌ | ❌ | ✅ Native |
| SvelteKit support | ⚠️ Manual config | ⚠️ | ✅ Official |
| Nuxt 3 support | ⚠️ Manual config | ⚠️ | ✅ Official |
| GitHub Stars | ~44K | ~44K | ~14K |
| Weekly downloads | ~20M | — | ~9M |
ESM Support: The Critical Difference
Jest's ESM story has been "experimental" since 2020. Jest 30 promotes it to stable, but the experience is still different from Vitest's.
// Jest 30: Native ESM requires package.json "type": "module"
// OR renaming files to .mjs/.mts
// jest.config.mjs
export default {
testEnvironment: 'node',
// Still need explicit transform config for CJS interop
transform: {},
extensionsToTreatAsEsm: ['.ts'],
};
// package.json
{
"type": "module"
}
// Warning: this breaks many tools that expect CJS by default
// Vitest: ESM just works, no package.json changes needed
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
// TypeScript + ESM, zero config
},
});
The practical difference: with Jest 30, you still need to audit your dependencies for CJS/ESM boundary issues. Libraries that ship only CJS need transformIgnorePatterns tuning. With Vitest, the Vite plugin pipeline handles this automatically — it normalizes the module format.
Coverage Providers
Both support V8 and Istanbul coverage, but with different defaults:
# Vitest — two providers, choose one
npm install --save-dev @vitest/coverage-v8
# or
npm install --save-dev @vitest/coverage-istanbul
# vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: 'v8', // or 'istanbul'
reporter: ['text', 'lcov', 'html'],
include: ['src/**/*.{ts,tsx}'],
exclude: ['**/*.test.ts', '**/*.spec.ts'],
thresholds: {
lines: 80,
branches: 75,
},
},
},
});
# Jest — Istanbul is built-in, V8 via separate package
npm install --save-dev jest-v8-coverage # for V8 provider
# jest.config.ts
export default {
collectCoverageFrom: ['src/**/*.{ts,tsx}'],
coverageThreshold: {
global: {
lines: 80,
branches: 75,
},
},
};
V8 coverage (native Node.js) is faster and more accurate for source maps. Istanbul is more configurable and generates better HTML reports. For most CI setups, @vitest/coverage-v8 is the right default — faster collection, no instrumentation pass needed.
Coverage timing comparison (same 300-file TypeScript project):
| Provider | Jest time | Vitest time |
|---|---|---|
| Istanbul | +18s | +8s |
| V8 | +12s | +5s |
Snapshot Testing: Where Jest Still Leads
Vitest's snapshot testing is API-compatible but has some rough edges:
// Custom serializers — Jest has a larger ecosystem
// jest.config.ts
module.exports = {
snapshotSerializers: [
'enzyme-to-json/serializer',
'jest-serializer-html',
],
};
// Vitest — must use expect.addSnapshotSerializer()
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
snapshotFormat: {
indent: 2,
printBasicPrototype: false,
},
},
});
// Inline snapshots — both work, formatting slightly differs
// Jest output:
expect(result).toMatchInlineSnapshot(`
Object {
"id": 1,
"name": "Alice",
}
`);
// Vitest output:
expect(result).toMatchInlineSnapshot(`
{
"id": 1,
"name": "Alice",
}
`);
// Note: Vitest omits "Object" prefix — cleaner output, but snapshot diffs if you migrate
If you have thousands of existing Jest snapshots and migrate to Vitest, run vitest --update-snapshot after migration. Many snapshots will change due to formatting differences (the Object prefix removal is the most common change).
Migration Guide: Jest → Vitest
Step 1: Install
# Remove Jest
npm remove jest ts-jest @types/jest jest-environment-jsdom babel-jest @babel/core
# Install Vitest
npm install --save-dev vitest @vitest/coverage-v8
# For React component tests (jsdom environment)
npm install --save-dev jsdom @testing-library/jest-dom
# For UI dashboard (optional)
npm install --save-dev @vitest/ui
Step 2: Config
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react'; // if React project
import path from 'path';
export default defineConfig({
plugins: [react()], // remove if not React
test: {
environment: 'jsdom', // 'node' for backend projects
globals: true, // optional: enables global describe/it/expect without imports
setupFiles: ['./src/test-setup.ts'],
coverage: {
provider: 'v8',
include: ['src/**/*.{ts,tsx}'],
exclude: ['**/*.test.{ts,tsx}'],
},
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
});
Step 3: Update package.json Scripts
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui"
}
}
Step 4: Update Imports
// Before (Jest with globals)
// No imports needed if using Jest globals
// After (Vitest with globals: true)
// Still no imports needed — same behavior
// After (Vitest without globals — recommended for explicit imports)
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
Step 5: Replace jest.* with vi.*
// Find and replace across test files
jest.fn() → vi.fn()
jest.mock() → vi.mock()
jest.spyOn() → vi.spyOn()
jest.clearAllMocks() → vi.clearAllMocks()
jest.resetAllMocks() → vi.resetAllMocks()
jest.restoreAllMocks() → vi.restoreAllMocks()
jest.useFakeTimers() → vi.useFakeTimers()
jest.useRealTimers() → vi.useRealTimers()
jest.runAllTimers() → vi.runAllTimers()
jest.advanceTimersByTime() → vi.advanceTimersByTime()
Common Migration Gotchas
Gotcha 1: __mocks__ directory behavior differs
Error: Cannot find module '@/utils/api'
Jest auto-mocks files in __mocks__ adjacent to node_modules. Vitest requires explicit opt-in:
// vitest.config.ts — enable Jest-compatible automocking
export default defineConfig({
test: {
// Vitest doesn't auto-mock __mocks__ by default
// You must call vi.mock() explicitly, or enable:
// automock: true (available in Vitest 3)
},
});
Gotcha 2: moduleNameMapper → alias
// jest.config.ts
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|scss)$': '<rootDir>/__mocks__/styleMock.js',
}
// vitest.config.ts
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
// CSS modules — handled automatically by Vite's CSS pipeline
// No need for a style mock if using Vite
Gotcha 3: Dynamic require() in ESM context
ReferenceError: require is not defined
Vitest runs in ESM mode. If your source code or tests use require():
// Problem: Legacy require in source file
const config = require('./config.json');
// Fix option 1: Use import
import config from './config.json' assert { type: 'json' };
// Fix option 2: Use createRequire in Vitest test
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const config = require('./config.json');
Gotcha 4: testEnvironment declaration file
Cannot find name 'expect'. Do you need to install type definitions?
With Jest, @types/jest adds global types automatically. With Vitest:
// tsconfig.json — add vitest globals
{
"compilerOptions": {
"types": ["vitest/globals"]
}
}
// OR in individual test files (if not using globals mode):
import { expect, describe, it } from 'vitest';
Gotcha 5: Snapshot format differences
snapshot mismatch: received "{ id: 1 }" expected "Object { id: 1 }"
Run vitest --update-snapshot after migration. The formatting changes are cosmetic — the actual test behavior is the same. Include the snapshot updates in your migration PR.
Gotcha 6: jest.isolateModules() → vi.isolateModules()
The API exists in Vitest but with slightly different behavior around ESM module caching. If you rely on isolateModules() for module state isolation between tests, test thoroughly after migration — the ESM module cache invalidation semantics differ.
Gotcha 7: Timer mocking with async/await
// Jest — this pattern sometimes requires manual act() wrapping
jest.useFakeTimers();
render(<Component />);
act(() => jest.advanceTimersByTime(1000));
// Vitest — use vi.useFakeTimers() + await vi.runAllTimersAsync()
vi.useFakeTimers();
render(<Component />);
await vi.runAllTimersAsync(); // handles Promise microtasks properly
CI Pipeline Impact
Real numbers from teams who completed migration:
Team A: Next.js SaaS, 280 test files
- Before (Jest + ts-jest): 4m 12s CI test step
- After (Vitest): 58s CI test step
- Saving: ~3m 14s per CI run, ~$180/month on GitHub Actions at scale
Team B: TypeScript monorepo, 12 packages
- Before (Jest): 8m 45s full test suite
- After (Vitest): 2m 10s full test suite
- Saving: ~6m 35s per run
Team C: Vite + React, 350 test files
- Before (Jest + babel-jest): 6m 30s
- After (Vitest): 1m 22s
- Saving: 4m 48s per run
The ROI calculation for Jest → Vitest migration is straightforward if you're paying for CI minutes. A team running 50 CI runs/day on a 6-minute test suite saves ~250 CI minutes/day. At typical cloud CI rates, that's real money within weeks.
When to Stay on Jest
Not every team should migrate. Genuine reasons to stay:
1. React Native. The jest-expo preset, React Native-specific mocks, and Metro bundler integration have no Vitest equivalent. RN teams should stay on Jest until the ecosystem catches up.
2. Angular. @angular/core/testing and Angular's TestBed are Jest-compatible via jest-preset-angular. Vitest community support exists but is not officially backed by the Angular team.
3. You have a massive, stable legacy Jest suite. If you have 2,000+ test files, a stable Jest config that runs in 2 minutes (well-optimized), and no plans for Vite adoption, the migration effort isn't worth it. The speed wins are largest for TypeScript + Vite projects.
4. You rely on Jest-specific serializers. If you're using enzyme-to-json/serializer, jest-serializer-vue, or custom snapshot serializers with complex behavior, verify compatibility before migrating.
5. Your team just completed a Jest upgrade. If you just migrated from Jest 27 to Jest 29 + ts-jest + updated all your config, the marginal speed gain doesn't justify doing it again immediately.
Vitest 3.0: What's New (January 2026)
The January 2026 release added features that close remaining gaps:
Browser mode (stable): @vitest/browser uses Playwright or WebdriverIO to run tests in a real browser — not jsdom. For testing Web APIs, Canvas, IndexedDB, and other browser-native APIs that jsdom emulates imperfectly, this is a significant win.
// vitest.config.ts — browser mode
export default defineConfig({
test: {
browser: {
enabled: true,
name: 'chromium',
provider: 'playwright',
},
},
});
Worker process isolation: Vitest 3 adds pool: 'forks' option — spawns separate Node.js processes like Jest instead of worker threads. Useful when testing code with native addons or when worker threads cause issues.
export default defineConfig({
test: {
pool: 'forks', // Jest-like process isolation
// pool: 'threads', // default, faster
// pool: 'vmThreads', // VM context isolation per test file
},
});
Vite 6 compatibility: Full support for Vite 6's Rolldown bundler. Test transforms now use Rolldown's faster pipeline when available.
The Verdict for 2026
| Situation | Recommendation |
|---|---|
| New TypeScript + Vite project | Vitest — it's the obvious default |
| New TypeScript + non-Vite project | Vitest — still worth the setup |
| Existing Jest codebase, tests < 30s | Stay on Jest — migration cost > speed gain |
| Existing Jest codebase, tests > 2 min | Migrate to Vitest — CI cost savings alone justify it |
| React Native | Jest — no viable alternative |
| Angular | Jest — better official support |
| SvelteKit / Nuxt / Astro | Vitest — officially recommended |
| Monorepo, 500+ test files | Vitest — worker thread model scales better |
The choice is no longer theoretical. Vitest has a stable 3.0, growing ecosystem, and the benchmark data shows consistent 3–8x speed improvements on the projects where it matters most. For greenfield TypeScript projects in 2026, Jest is the legacy choice.
Related Articles
pnpm vs npm vs Yarn: Package Manager Guide 2026 — choosing a package manager affects install times in the same CI pipelines where testing speed matters
tRPC vs GraphQL 2026 — TypeScript-first API patterns that pair well with Vitest's TypeScript-native testing
Bun vs Node.js 2026 — using Bun as the runtime can further reduce test startup times, especially on Node.js backend tests
Compare Vitest and Jest package health on PkgPulse.
See the live comparison
View vitest vs. jest on PkgPulse →