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
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"],
},
},
})
3. Update package.json
{"scripts":{"test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","test:ui":"vitest --ui"}}
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" {} +
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" }) }))
Timer mocks — identical API
vi.useFakeTimers()
vi.runAllTimers()
vi.useRealTimers()
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",
},
},
})
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.