Vitest 2.0 vs Jest: We Migrated 400 Tests and Here's What Actually Changed

javascript dev.to

Vitest 2.0 vs Jest: We migrated a 400-test Jest suite last month. Runtime dropped from 47 seconds to 11 seconds. Here is the honest breakdown.

Why Vitest 2.0 Is Worth the Switch

Vitest 1.x was already faster than Jest for TypeScript projects because it reuses Vite's transform pipeline instead of running ts-jest or babel-jest. Vitest 2.0 adds:

  • Browser mode — run tests in Chromium/Firefox/WebKit via Playwright
  • Improved workspace — monorepo-aware with shared configs
  • Coverage v8 integrated without extra configuration
  • Snapshot serializers now match Jest's format exactly (v1 migration blocker is gone)

Migration Steps

1. Install

npm uninstall jest @types/jest ts-jest jest-environment-jsdom
npm install -D vitest @vitest/coverage-v8 jsdom @testing-library/jest-dom
Enter fullscreen mode Exit fullscreen mode

2. Create vitest.config.ts

import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
import tsconfigPaths from "vite-tsconfig-paths"

export default defineConfig({
  plugins: [react(), tsconfigPaths()],
  test: {
    environment: "jsdom",
    globals: true,
    setupFiles: ["./src/test/setup.ts"],
    coverage: {
      provider: "v8",
      reporter: ["text", "html", "lcov"],
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

3. Update package.json

{"scripts":{"test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","test:ui":"vitest --ui"}}
Enter fullscreen mode Exit fullscreen mode

4. Bulk replace jest -> vi

With globals: true, existing test files need only one change — replace jest.mock/jest.fn/jest.spyOn with vi equivalents:

find src -name "*.test.*" -exec sed -i "" "s/jest\.mock/vi.mock/g" {} +
find src -name "*.test.*" -exec sed -i "" "s/jest\.fn/vi.fn/g" {} +
find src -name "*.test.*" -exec sed -i "" "s/jest\.spyOn/vi.spyOn/g" {} +
Enter fullscreen mode Exit fullscreen mode

The Gotchas

Module mocking scope

Vitest hoists vi.mock() statically. Variables from outer scope inside the factory cause ReferenceError:

// Breaks: outer variable referenced inside vi.mock factory
const mockUser = { id: "1" }
vi.mock("./auth", () => ({ getUser: () => mockUser }))

// Works: value defined inline
vi.mock("./auth", () => ({ getUser: () => ({ id: "1" }) }))
Enter fullscreen mode Exit fullscreen mode

Timer mocks — identical API

vi.useFakeTimers()
vi.runAllTimers()
vi.useRealTimers()
Enter fullscreen mode Exit fullscreen mode

ESM modules

Remove __esModule: true from any mocks — Vitest handles ESM natively and does not need it.

Performance Comparison

Our project: 400 tests, TypeScript, React, service layer with DB mocks.

Runner Cold run Watch (incremental)
Jest + ts-jest 47s 12s per change
Vitest 2.0 11s 1.2s per change

The watch mode improvement is the real productivity win.

Browser Mode

For components that rely on real browser APIs (ResizeObserver, canvas, clipboard):

export default defineConfig({
  test: {
    browser: {
      enabled: true,
      provider: "playwright",
      name: "chromium",
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

Run with vitest --browser. Tests execute in real Chromium — slower than jsdom but accurate for UI-heavy components.

When to Stay on Jest

  • Large team with Jest expertise and no performance pain
  • Heavy CJS module usage where ESM migration is too costly
  • Relies on Jest-specific custom matcher ecosystem

For any greenfield TypeScript + Vite/Next.js project, start with Vitest.


Shipping a TypeScript SaaS that needs a solid foundation? The Ship Fast Skill Pack includes testing patterns, Vitest setup, and 20+ production-ready Claude Code skills for $49.

Source: dev.to

arrow_back Back to Tutorials