Skip to main content

Vitest 3 vs Jest 30: Testing in 2026

·PkgPulse Team
0

The JavaScript testing landscape consolidated hard in 2025-2026. Vitest 3 shipped with a redesigned public API, inline workspace configuration, and a mature browser mode backed by Playwright and WebdriverIO. Jest 30 shipped in June 2025 with ESM improvements, a smaller bundle, and dropped Node.js 14/16 support. Both frameworks are production-ready. The question is which one belongs in your stack.

The short answer: Vitest is faster, integrates natively with Vite, and has better TypeScript DX. Jest still makes sense if you're on a CommonJS-only codebase, already invested in Jest's snapshot format and mocking patterns, or running a very large test suite where you've tuned Jest's sharding.

TL;DR

New Vite/TypeScript projects: Vitest — 5.6x faster cold starts, native ESM, no config required for TypeScript. Existing Jest codebases: Migrate when Vitest's speed savings justify the one-time migration cost (usually 1-3 days). Legacy CommonJS projects: Jest 30 is safe, maintained, and faster than Jest 29.

Key Takeaways

  • Vitest 3: 5.6x faster cold starts (38s vs 214s), 28x faster watch mode re-runs (0.3s vs 8.4s), 57% lower peak memory
  • Jest 30: Drops Node 14/16/19/21, ESM wrapper support, jsdom 26, bundled packages for faster startup, .mts/.cts module extensions
  • API compatibility: Vitest is ~95% API-compatible with Jest — describe, it, expect, vi.mock mirrors jest.mock
  • Migration cost: Most projects migrate in 1-3 days; the main hurdles are jest.mock() factory patterns and manual mocks
  • Browser mode: Vitest 3 has stable browser mode with Playwright/WebdriverIO; Jest uses jsdom (not a real browser)

At a Glance

Vitest 3Jest 30
Cold start (500 tests)~38s~214s
Watch mode re-run~0.3s~8.4s
Peak memory~400MB~930MB
Native ESMYesPartial (wrappers)
TypeScriptNative via ViteRequires ts-jest or babel
Browser modePlaywright/WebdriverIOjsdom only
Inline workspace configYes (projects array)No
Snapshot formatCompatible with JestJest format

Vitest 3: What's New

Vitest 3 shipped in early 2025 and the 3.x series through early 2026 has focused on API stability, browser mode maturity, and monorepo support.

Inline Workspace Configuration

In Vitest 2.x, monorepo projects needed a separate vitest.workspace.ts file to define test projects. Vitest 3 deprecated the standalone workspace file in favor of an inline projects array in your root vitest.config.ts:

// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    projects: [
      './packages/*/vitest.config.ts',
      {
        test: {
          name: 'unit',
          environment: 'node',
          include: ['**/*.unit.test.ts'],
        },
      },
      {
        test: {
          name: 'browser',
          browser: {
            enabled: true,
            provider: 'playwright',
            instances: [{ browser: 'chromium' }],
          },
          include: ['**/*.browser.test.ts'],
        },
      },
    ],
  },
})

The workspace terminology was also deprecated because it conflicts with PNPM's workspace concept, which confused developers working in monorepos.

Browser Mode

Vitest 3's browser mode runs tests in a real browser instance via Playwright or WebdriverIO, using a single Vite server to serve all files. This is architecturally different from Jest's jsdom — jsdom simulates the DOM in Node.js, which means CSS, layout, animations, and browser-specific APIs like IntersectionObserver require mocking. Vitest's browser mode runs in Chromium, Firefox, or WebKit with no simulation layer.

The caching improvement in browser mode is significant: Vitest creates one Vite server regardless of how many browser instances are configured. Files are processed once and reused, making large browser test suites dramatically faster than running isolated jsdom tests per file.

Redesigned Public API

Vitest 3 redesigned vitest/node to expose a stable programmatic API for running tests from scripts, CI orchestration tools, or custom test runners. All methods are now documented and versioned:

import { startVitest } from 'vitest/node'

const vitest = await startVitest('test', [], {
  reporter: 'verbose',
  coverage: { enabled: true },
})
await vitest.close()

Annotation API

Tests can now attach metadata to test runs:

import { test, annotate } from 'vitest'

test('processes payment', async ({ annotate }) => {
  await annotate('payment-id', '12345')
  // visible in HTML reporter, JUnit, GitHub Actions reporter
})

Filter by Line Number

Running a specific test by file and line:

vitest run src/utils.test.ts:42

This speeds up the feedback loop when debugging a single failing test without needing to remember the exact test name.

Jest 30: What's New

Jest 30 shipped June 2025 as the first major release since Jest 29 in 2022. The theme is modernization: drop old Node.js versions, improve ESM support, reduce memory usage.

Dropped Node.js Versions

Jest 30 requires Node.js 18+. Versions 14, 16, 19, and 21 are no longer supported. For teams on Node.js 18 LTS (maintenance mode) or 20/22/24 LTS, this has no impact. For teams still running Node.js 16, upgrading Node.js is now a prerequisite.

ESM Improvements

Jest 30 adds ESM wrappers to its own packages, laying groundwork for Jest itself to run in an ESM context. The new moduleFileExtensions now includes .mts and .cts by default, so TypeScript ESM modules are recognized without manual configuration. import.meta.* and file:// URLs now work when using native ESM with Jest.

Note: Jest's ESM support still requires either --experimental-vm-modules in Node.js or Babel transform. It's improved but not as seamless as Vitest's native ESM execution.

Bundled Packages

Jest 30 bundles each package into a single file. This reduces require() calls during Jest's own startup, producing noticeable cold start improvements. Large test suites that previously waited 8-10 seconds before the first test ran now start in 4-6 seconds.

jsdom 26

jest-environment-jsdom upgrades from jsdom 21 to 26. jsdom 26 adds better Web Crypto support, improved fetch compatibility, and fixes for several edge cases in form element handling. Teams using jest-environment-jsdom for React component tests get these fixes without code changes.

Breaking Changes Summary

- Node 14, 16, 19, 21 no longer supported
- Minimum TypeScript: 5.4
- jest-environment-jsdom: jsdom 21 → 26
- Non-enumerable object properties excluded from toEqual by default
- --testPathPattern renamed to --testPathPatterns (plural)
- Removed expect aliases: toBeCalled, toBeCalledWith, toBeCalledTimes, etc.
  (use toHaveBeenCalled, toHaveBeenCalledWith, toHaveBeenCalledTimes)
- Jest internals no longer accessible from node_modules

Speed Comparison

The performance gap between Vitest and Jest is architectural, not incidental. Understanding why helps set realistic expectations.

Why Vitest is Faster

Vitest uses Vite's module graph to track import relationships. When you change utils/date.ts, Vitest knows exactly which test files import it (transitively) and reruns only those. Jest's --onlyChanged uses git diff heuristics, which is less precise and causes more unnecessary reruns.

For transforms, Vitest uses esbuild (via Vite) to compile TypeScript and JSX. esbuild compiles TypeScript 10-100x faster than tsc. Jest's ts-jest uses the TypeScript compiler, and even babel-jest is slower than esbuild for large files.

Benchmark Numbers

From production monorepo testing (real-world, not synthetic):

Cold start (500 test files, TypeScript, React components):

  • Vitest 3: ~38 seconds
  • Jest 30: ~214 seconds
  • Speedup: 5.6x

Watch mode re-run (change one utility function, 3 affected test files):

  • Vitest 3: ~0.3 seconds
  • Jest 30: ~8.4 seconds (full git-diff-based reruns)
  • Speedup: 28x

Memory (peak during full suite run):

  • Vitest 3: ~400MB
  • Jest 30: ~930MB
  • Reduction: 57%

For small projects with under 100 tests, the difference is less noticeable (both complete in seconds). The gap compounds with project size.

Configuration Comparison

Vitest

// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['./src/test/setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html'],
      include: ['src/**/*.{ts,tsx}'],
      exclude: ['src/**/*.d.ts'],
    },
  },
})

No separate TypeScript config for tests. No transform configuration. Vitest reuses your existing vite.config.ts plugins automatically if you don't create a separate vitest.config.ts.

Jest 30

// jest.config.js
/** @type {import('jest').Config} */
module.exports = {
  testEnvironment: 'jsdom',
  transform: {
    '^.+\\.(ts|tsx)$': ['ts-jest', { tsconfig: './tsconfig.test.json' }],
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '\\.(css|scss)$': 'identity-obj-proxy',
  },
  setupFilesAfterFramework: ['./src/test/setup.ts'],
  collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts'],
  coverageReporters: ['text', 'html'],
}

Jest requires explicit transform configuration for TypeScript. CSS and asset imports need manual mocking via moduleNameMapper. Path aliases must be duplicated from tsconfig.json into moduleNameMapper.

Mocking Comparison

Vitest

import { vi, describe, it, expect, beforeEach } from 'vitest'
import { fetchUser } from '../api'

vi.mock('../api', () => ({
  fetchUser: vi.fn(),
}))

describe('UserProfile', () => {
  beforeEach(() => {
    vi.mocked(fetchUser).mockResolvedValue({ id: '1', name: 'Alice' })
  })

  it('renders user name', async () => {
    // ...
  })
})

Jest 30

import { jest, describe, it, expect, beforeEach } from '@jest/globals'
import { fetchUser } from '../api'

jest.mock('../api', () => ({
  fetchUser: jest.fn(),
}))

describe('UserProfile', () => {
  beforeEach(() => {
    jest.mocked(fetchUser).mockResolvedValue({ id: '1', name: 'Alice' })
  })

  it('renders user name', async () => {
    // ...
  })
})

The APIs are nearly identical. The main difference is vi vs jest. Vitest's vi object has the same surface area as Jest's jest object.

Migration Guide: Jest → Vitest

Most migrations complete in 1-3 days. Here's the systematic approach.

Step 1: Install Vitest

npm install -D vitest @vitest/coverage-v8
# If using React:
npm install -D @vitejs/plugin-react jsdom
# If using browser mode:
npm install -D @vitest/browser playwright

Step 2: Create vitest.config.ts

import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true, // Makes describe/it/expect available without imports
    environment: 'jsdom',
    setupFiles: ['./src/setupTests.ts'],
  },
})

Step 3: Update package.json Scripts

{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "test:coverage": "vitest run --coverage"
  }
}

Step 4: Replace jest Imports

# Find all files importing from jest
grep -r "from '@jest/globals'" src/ --include="*.ts" -l

# Replace jest global with vi
# jest.fn() → vi.fn()
# jest.mock() → vi.mock()
# jest.spyOn() → vi.spyOn()
# jest.clearAllMocks() → vi.clearAllMocks()

If you use globals: true in vitest.config.ts, you don't need to import describe, it, expect — they're automatically available, just like Jest's default behavior.

Step 5: Fix jest.mock() Factory Issues

The most common migration issue: Vitest hoists vi.mock() calls to the top of the file (like Jest does), but the factory function cannot reference variables defined in the module scope. This is the same constraint as Jest but some codebases rely on patterns that technically work in Jest due to implementation differences.

// This may fail in Vitest:
const mockUser = { id: '1', name: 'Alice' }
vi.mock('../api', () => ({ fetchUser: vi.fn().mockReturnValue(mockUser) }))

// Fix: use vi.mocked() in beforeEach instead
vi.mock('../api')
beforeEach(() => {
  vi.mocked(fetchUser).mockReturnValue({ id: '1', name: 'Alice' })
})

Step 6: Update Snapshot Files

Vitest's snapshot format is identical to Jest's. Existing .snap files work without changes. If you have inline snapshots using toMatchInlineSnapshot, those also work as-is.

TypeScript Support

Vitest has native TypeScript support through Vite's esbuild pipeline. You get type checking in your editor for test code without additional packages. Type inference works across mock implementations, vi.mocked() returns properly typed mocks, and test utilities like expect.extend() are fully typed.

Jest 30 requires ts-jest or @babel/preset-typescript. ts-jest performs full type-checking during tests (slower but catches type errors). babel-jest strips types without checking them (faster but misses type errors). Neither is as ergonomic as Vitest's zero-config TypeScript.

Snapshot Testing

Both frameworks use the same snapshot format. The workflow is identical:

it('renders correctly', () => {
  const { asFragment } = render(<Button label="Click me" />)
  expect(asFragment()).toMatchSnapshot()
})

Update snapshots: vitest run --update-snapshots or jest --updateSnapshot.

Coverage

Vitest supports two coverage providers: v8 (native V8 coverage, fast, zero config) and istanbul (same as Jest, more accurate for some edge cases). Jest uses istanbul exclusively.

For most projects, Vitest's v8 coverage is sufficient and meaningfully faster. If you need the exact same coverage numbers as Jest for compliance reporting, use @vitest/coverage-istanbul.

When to Stick with Jest

  • CommonJS-only environment: If you can't adopt ESM, Jest 30 handles CJS more smoothly
  • Very large test suites with sharding: Jest's --shard implementation is more mature; Vitest added sharding in v1 but Jest's is more battle-tested for 10K+ test suites
  • Team already productive in Jest: Migration has a cost; if your tests pass and CI times are acceptable, there's no emergency
  • Angular projects: Angular's testing ecosystem is built around Jest and Jasmine; Vitest works but the ecosystem support is thinner

CI/CD Integration

Running tests in CI is where configuration differences between Vitest and Jest become most visible. Both support parallelization and sharding, but their approaches differ in practical ways.

GitHub Actions: Vitest

# .github/workflows/test.yml
name: Test
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npx vitest run --shard=${{ matrix.shard }}/4 --reporter=junit --outputFile=test-results/junit-${{ matrix.shard }}.xml
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results-${{ matrix.shard }}
          path: test-results/

Vitest's --shard=N/M flag splits the test suite into M shards and runs shard N. The matrix strategy runs all shards in parallel, reducing total CI time proportionally. A 4-shard setup for a 500-test suite cuts a 38-second run down to approximately 10-12 seconds wall time.

GitHub Actions: Jest 30

name: Test
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npx jest --shard=${{ matrix.shard }}/4 --ci --forceExit --reporters=default --reporters=jest-junit
        env:
          JEST_JUNIT_OUTPUT_DIR: test-results
          JEST_JUNIT_OUTPUT_NAME: junit-${{ matrix.shard }}.xml
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results-${{ matrix.shard }}
          path: test-results/

Jest requires --ci to disable interactive watch mode in CI environments and --forceExit to terminate the process after tests complete (Jest sometimes hangs waiting for open handles). The jest-junit package must be installed separately for JUnit output; Vitest includes JUnit as a built-in reporter.

Parallelization Strategy

Both frameworks run test files in parallel by default using worker threads. Key configuration options:

Vitest:

// vitest.config.ts
export default defineConfig({
  test: {
    pool: 'threads',          // 'threads' (default) | 'forks' | 'vmThreads'
    poolOptions: {
      threads: {
        maxThreads: 4,        // Cap workers (useful in memory-constrained CI)
        minThreads: 2,
      },
    },
    fileParallelism: true,    // Run files in parallel (default: true)
    isolate: true,            // Fresh module registry per file (default: true)
  },
})

Jest 30:

// jest.config.js
module.exports = {
  maxWorkers: '50%',          // Use 50% of available CPUs (default in CI)
  workerThreads: true,        // Use worker_threads instead of child_process (Jest 29+)
  testTimeout: 10000,
  forceExit: true,            // Prevents CI hangs from open handles
}

Vitest's pool: 'vmThreads' option uses V8 context isolation (lighter than full worker threads), which reduces memory usage at the cost of slightly less isolation. This is useful for memory-constrained CI runners where --max-old-space-size limits apply.

Coverage in CI

For projects that enforce coverage thresholds in CI, both frameworks support failing the build when thresholds aren't met:

// vitest.config.ts
export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 70,
        statements: 80,
      },
      reporter: ['text', 'lcov'],  // lcov for Codecov/Coveralls upload
    },
  },
})
// jest.config.js
module.exports = {
  coverageThreshold: {
    global: {
      lines: 80,
      functions: 80,
      branches: 70,
      statements: 80,
    },
  },
  coverageReporters: ['text', 'lcov'],
}

Both exit with a non-zero code when thresholds aren't met, failing the CI run. Vitest's v8 coverage generates these reports 2-3x faster than Jest's istanbul, which matters for large codebases that run coverage on every PR.

Comments

Get the 2026 npm Stack Cheatsheet

Our top package picks for every category — ORMs, auth, testing, bundlers, and more. Plus weekly npm trend reports.