Skip to main content

Vitest vs Jest: Speed Benchmarks 2026

·PkgPulse Team
0

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

Before the benchmarks, the adoption numbers:

PackageWeekly DownloadsGitHub StarsTrend
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.

MetricJest 29 + ts-jestJest 30 (native ESM)Vitest 3
Cold start68s52s14s
Warm run41s38s11s
Watch (1 file change)38s34s3.2s
CI (no cache)74s56s16s

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.

MetricJest 29Jest 30Vitest 3
Cold start22s18s16s
Warm run14s12s10s
Watch (1 file change)12s10s4s
CI (no cache)26s20s18s

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.

MetricJest 29 + babel-jestJest 30Vitest 3
Cold start94s71s18s
Warm run56s48s13s
Watch (1 file change)51s44s2.8s
CI (no cache)101s76s21s

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.

MetricJest 29 + ts-jestJest 30Vitest 3
Cold start112s84s32s
Warm run68s54s24s
Watch (1 file change)62s48s6s
CI (no cache)118s89s36s

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.

MetricJest 29 (--runInBand)Jest 29 (parallel)Vitest 3 (parallel)
Full suite cold180s94s38s
Full suite warm115s62s28s
Watch (1 package)88s45s4.5s
CI (no cache)195s102s42s

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.

MetricJest 29 (jest-expo)Vitest 3
Cold start45sNot 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)

MetricJest 29Vitest 3
Cold start51s12s
Warm run32s9s
Watch (1 file change)28s2.4s
CI (no cache)56s14s

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)

MetricJest 29Vitest 3
Cold start72s17s
Warm run44s12s
Watch (1 file change)41s3.1s
CI (no cache)79s19s

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)

MetricJest 29Vitest 3
Cold start38s8s
Warm run24s6s
Watch (1 file change)22s1.8s
CI (no cache)42s9s

10. Large Express Backend (TypeScript, 400 test files, Prisma + Supertest)

MetricJest 29 + ts-jestJest 30Vitest 3
Cold start89s66s26s
Warm run55s42s18s
Watch (1 file change)50s38s5s
CI (no cache)95s71s29s

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

FeatureJest 29Jest 30Vitest 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):

ProviderJest timeVitest 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: moduleNameMapperalias

// 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

SituationRecommendation
New TypeScript + Vite projectVitest — it's the obvious default
New TypeScript + non-Vite projectVitest — still worth the setup
Existing Jest codebase, tests < 30sStay on Jest — migration cost > speed gain
Existing Jest codebase, tests > 2 minMigrate to Vitest — CI cost savings alone justify it
React NativeJest — no viable alternative
AngularJest — better official support
SvelteKit / Nuxt / AstroVitest — officially recommended
Monorepo, 500+ test filesVitest — 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.


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.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.